java.util.concurrent中的CopyOnWrite容器(转载)

今天在CoolShell上看到有人在介绍CopyOnWrite容器,突然一看觉得非常高大上,然后突然一看原来是java.util.concurrent中早就实现好的类,瞬间觉得自己j2se白学了,连这些成熟的实现都不知道。但是仔细一想还是在工作中没有这样的应用场景,所以导致好多东西都弱化了。

言归正传,看看人家是怎么讲CopyOnWrite容器的吧。

以下内容转载自酷壳:

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向ArrayList里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public boolean add(T e) { 
    final ReentrantLock lock = this.lock; 
    lock.lock(); 
    try
    
        Object[] elements = getArray(); 
    
        int len = elements.length; 
        // 复制出新数组 
    
        Object[] newElements = Arrays.copyOf(elements, len + 1); 
        // 把新元素添加到新数组里 
    
        // 把原数组引用指向新数组          newElements[len] = e; 
    
        setArray(newElements); 
    
        return true
    
    } finally
    
        lock.unlock(); 
    
    
    
    
final void setArray(Object[] a) { 
    array = a; 
}

读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

1
public E get(int index) {     return get(getArray(), index); }

JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Collection; 
import java.util.Map; 
import java.util.Set;  
  
 public class CopyOnWriteMap<K, V> implements Map<K,V>,Cloneable 
{   private volatile Map<K, V> internalMap;      
    public CopyOnWriteMap() {        
          internalMap = new HashMap<K, V>();   
     }      
 public V put(K key, V value) {           
      synchronized (this) {           
     Map<K, V> newMap = new HashMap<K, V>(internalMap);             
     V val = newMap.put(key, value);            
      internalMap = newMap;            
         return val;         }     }      
 public V get(Object key) {         
      return internalMap.get(key);     }      
 public void putAll(Map<? extends K, ? extends V> newData)  {         synchronized (this) {            
                Map<K, V> newMap = new HashMap<K,V>          (internalMap);             newMap.putAll(newData);             internalMap = newMap;         }     } }

CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

 

packagecom.ifeve.book; 
 
importjava.util.Map; 
 
importcom.ifeve.book.forkjoin.CopyOnWriteMap; 
 
/** 
 * 黑名单服务 
 
 * @author fangtengfei 
 
 */
publicclassBlackListServiceImpl { 
 
    privatestaticCopyOnWriteMap<String, Boolean> blackListMap = newCopyOnWriteMap<String, Boolean>( 
            1000); 
 
    publicstaticbooleanisBlackList(String id) { 
        returnblackListMap.get(id) == nullfalsetrue
    
 
    publicstaticvoidaddBlackList(String id) { 
        blackListMap.put(id, Boolean.TRUE); 
    
 
    /** 
     * 批量添加黑名单 
     
     * @param ids 
     */
    publicstaticvoidaddBlackList(Map<String,Boolean> ids) { 
        blackListMap.putAll(ids); 
    
 

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap

数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

关于C++的STL中,曾经也有过Copy-On-Write的玩法,参见陈皓的《C++ STL String类中的Copy-On-Write》,后来,因为有很多线程安全上的事,就被去掉了。

 

RMI 数据源方式

相关资料:ObjectWeb  http://rmijdbc.ow2.org/
下载驱动:RmiJdbc.jar
 
Make your CLASSPATH variable point on the RmiJdbc.jar package: it is located in the dist/lib subdirectory of the RmiJdbc distribution
  • Unix example (bash style):
    export CLASSPATH=$CLASSPATH:/usr/local/RmiJdbc/dist/lib/RmiJdbc.jar
  • Windows example:
    set CLASSPATH=%CLASSPATH%;C:\RmiJdbc\dist\lib\RmiJdbc.jar

Start the RmiJdbc Server component:

java org.objectweb.rmijdbc.RJJdbcServer [-noreg] [-port regportnum] [-lp portnum] [-sm] [-ssl] [driverList]

Options:

  • -noreg: means you launch the RmiJdbc server with an external rmiregistry.
  • -port regportnum: specify the rmiregistry port (optional, useful if you launch rmiregistry on a port of your own).
  • -lp portnum: specify the listener port for remote objects (optional, useful if need to use a single port for remote objects – otherwise, dynamically allocated ports are used).
  • -sm: use the standard RMI SecurityManager (otherwise, a relaxed SecurityManager is used, equivalent to the RMI SecurityManager with AllPermissions set).
  • -ssl: use in SSL mode (see documentation for details).
  • driverList: list of JDBC Driver classes available on your server (ex. org.enhydra.instantdb.jdbc.idbDriver). You can also declare your driver list in the jdbc.drivers system property (java -Djdbc.drivers=driverList RmiJdbc.RJJdbcServer).

Then, a remote JDBC client can access your local database!

client code for example:

import java.sql.*;

import java.rmi.*;
import RmiJdbc.*;
import java.net.InetAddress;
/** * This is a sample program for RmiJdbc client/server jdbc Driver * RmiJdbc relies on Java RMI for jdbc objects distribution */
public class TestClient { 
 public static void main(String[] args) {    
      try {      
           // Register RmiJdbc Driver in jdbc DriverManager      
          // On some platforms with some java VMs, newInstance() is necessary...     
           Class.forName("org.objectweb.rmijdbc.Driver").newInstance();      
           // Test with InstantDB java database engine      
           // See http://www.lutris.com/products/instantDB/index.html      
            // for info & download     
            String url = "jdbc:idb:sample.prp";      // RMI host will point to local host     
          String rmiHost = new String(       "//" + InetAddress.getLocalHost().getHostName());      
           // RmiJdbc URL is of the form:     
          // jdbc:rmi://<rmiHostName[:port]>/<jdbc-url>      
          java.sql.Connection c = DriverManager.getConnection("jdbc:rmi:"       + rmiHost + "/" + url);             java.sql.Statement st = c.createStatement();     
    java.sql.ResultSet rs = st.executeQuery("SELECT * FROM import1");    
       java.sql.ResultSetMetaData md = rs.getMetaData();    
       while(rs.next()) {       
                  System.out.print("\nTUPLE: | ");      
                  for(int i=1; i<= md.getColumnCount(); i++) {        
                                 System.out.print(rs.getString(i) + " | ");        }      
                      }      rs.close();  
                    } catch(Exception e) {      e.printStackTrace();    }  }};
 
和Spring集成的方式(首先也需要Start the RmiJdbc Server component):
1.加入相应的Jar包
2.在applicationContext.xml中加入bean
     <bean id="wldtDataSource"class="org.springframework.jdbc.datasource.DriverManagerDataSource">
               <property name="driverClassName">
                     <value>org.objectweb.rmijdbc.Driver</value>
            </property>
           <property name="url">
                   <value>jdbc:rmi://172.20.52.26/jdbc:odbc:SiteWeaver</value><!--java默认1099端口-->
          </property></bean><bean id="wldtjdbcDAO" class="com.thunisoft.summer.dao.jdbc.JdbcDAO">
         <property name="dataSource">
                    <ref bean="wldtDataSource" />
        </property>
 </bean>
3.在DAO层注入数据源
        private JdbcTemplate getWldtJdbcTemplate()    {      
                return ((JdbcDAO) AppContext.getInstance().getAppContext().getBean( "wldtjdbcDAO")).getJdbcTemplate();    }
其中JdbcDAO继承了 JdbcDAO extends JdbcDaoSupport 在其中注入了相应的数据源,方便调用其提供的模版方法template
 

4.在RMI的server端还需要设置ODBC数据源

如图:

JVM调优总结-Xms -Xmx -Xmn -Xss(转载)

  1. 堆大小设置
    JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
    典型设置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

  1. 回收器选择
    JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置: java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy
:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
典型配置: java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

  1. 辅助信息
    JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

-XX:+PrintGC
输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]

                [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails
输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

                [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
输出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
输出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
输出形式:
34.702: [GC {Heap before gc invocations=7:
def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
 def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
}
, 0.0757599 secs]

  1. -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
  2. 常见配置汇总
  3. 堆设置
  4. -Xms:初始堆大小
  5. -Xmx:最大堆大小
  6. -XX:NewSize=n:设置年轻代大小
  7. -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  8. -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
  9. -XX:MaxPermSize=n:设置持久代大小
  10. 收集器设置
  11. -XX:+UseSerialGC:设置串行收集器
  12. -XX:+UseParallelGC:设置并行收集器
  13. -XX:+UseParalledlOldGC:设置并行年老代收集器
  14. -XX:+UseConcMarkSweepGC:设置并发收集器
  15. 垃圾回收统计信息
  16. -XX:+PrintGC
  17. -XX:+PrintGCDetails
  18. -XX:+PrintGCTimeStamps
  19. -Xloggc:filename
  20. 并行收集器设置
  21. -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
  22. -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  23. -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  24. 并发收集器设置
  25. -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  26. -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

四、调优总结

  1. 年轻代大小选择
  2. 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
  3. 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  4. 年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

    • 并发垃圾收集信息
    • 持久代并发收集次数
    • 传统GC信息
    • 花在年轻代和年老代回收上的时间比例
    • 减少年轻代和年老代花费的时间,一般会提高应用的效率

 

    1. 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
    2. 较小堆引起的碎片问题
      因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
    3. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    4. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

 

JAVA虚拟机内存区域(转载整理)

JVM的体系结构,如下图:

其中,Runtime DataArea(运行时数据区)是整个JVM的重点,平时,由于我们编写java程序很少关心内存的释放问题,这个都是JVM来自动管理的,不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。这里就大致的介绍一下JVM的这一区域。

JVM中,所有的数据和程序都存放在运行时数据区,如上图,这个区域又包括几个子区域,它们各自有各自的用途和生命周期, MethodArea和Heap是基于JVM实例的,即JVM的每个实例都有一个它自己的方法域和一个堆;PC Register和Stack是基于线程的,即每个线程创建的时候,都会拥有自己的程序计数器和栈;Native Method Stack是为虚拟机用到的Native方法服务。下面分别介绍这几个区域:

1.Heap(堆)

一个JVM实例只存在一个堆内存,对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:

a)Permanent Space(永久存储区)

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,一般被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

b)Young Generation Space(新生区)

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0space)和1区(Survivor 1space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。

c)Tenure Generation Space(养老区)

养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。

三个区的示意图如下:

之所以将堆内存再进行分区,主要是基于这样一个事实:不同对象的生命周期是不一样的。在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,对堆进行分区管理是采用了分治的思想,把不同生命周期的对象放在不同区域,不同区域采用最适合它的垃圾回收方式进行回收。分区之后可以提高JVM垃圾收集的效率,进而优化内存管理。

无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

2.Method Area(方法域)

方法域实际上就是堆中的永久存储区(Permanent Space),它还有个别名叫做Non-Heap(非堆),所以也可以将方法域看作堆的一个逻辑部分。方法域中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。这个区域除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久存储区不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。对类的卸载需要满足下面3个条件:
1)该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
2)加载该类的ClassLoader已经被GC;
3)该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

3.Stack(栈)

栈的生命周期也是与线程相同。栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个栈帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个栈帧在栈中的入栈至出栈的过程。栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“后进先出”原则。栈帧中主要保存3类数据:本地变量(LocalVariables),包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;栈帧数据(Frame Data),包括类文件、方法等等。

栈中有两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果栈可以动态扩展,当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

4.PC Register(程序计数器)

每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令。对于非Native方法,这个区域记录的是正在执行的VM原语的地址,该地址指向方法域中的方法字节码,由执行引擎读取下一条指令。如果正在执行的是Natvie方法,这个区域则为空。

5.Native Method Stack(本地方法栈)

本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。

这里对运行时数据区的几个逻辑组成部分做了个大致的介绍,其中,同样是存储数据的栈和堆有什么区别呢?这可能也是我们编码时容易忽略的地方。下一节来分析一下这个。

 

两种JNI方法注册方式

JNI方法注册方式有两种:

  • 通过javah产生函数头,该种方式产生的方法具有固定的格式。该方式使逆向分析人员比较容易获取java层native方法对应的本地方法。
  • 在JNI_OnLoad方法中手动注册jni方法,不易查找对应关系。

注意与RMI,WebService,CORBA(Common Object Request Broker Architecture,公共对象请求代理体系结构)的区别.

JNI的资料:

jni.pdf

 

将javabean转换成xml

使用到的第三方包:org.apache.commons.betwixt

代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import org.apache.commons.betwixt.io.BeanReader;
import org.apache.commons.betwixt.io.BeanWriter;
/**
     * 将javaBean 转成XML文件
     *
     * @param bean
     * javaBean对象
     * @return XML文件
     */
    public static File convertBean2XML(Object bean)
    {
        File file = new File(FjhlUtils.generateFileName(Consts.XML_FILE_SUFFIX));
        // FileWriter fw;
        BeanWriter beanWriter = null;
        OutputStreamWriter fw;
        try
        {
            fw = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
            beanWriter = new BeanWriter(fw);
            beanWriter.getXMLIntrospector().getConfiguration()
                    .setAttributesForPrimitives(false);
            beanWriter.getBindingConfiguration().setMapIDs(false);
            beanWriter.setEndOfLine("\n");
            beanWriter.enablePrettyPrint();
            beanWriter.setWriteEmptyElements(true);
            beanWriter.setEndTagForEmptyElement(true);
            beanWriter.writeXmlDeclaration(Consts.XML_TITLE);
            beanWriter.write(bean);
            beanWriter.flush();
        }
        catch (IOException e)
        {
            LOG.error("将javaBean 转成XML文件时发生IO异常", e);
        }
        catch (SAXException e)
        {
            LOG.error("将javaBean 转成XML文件时发生SAX解析异常", e);
        }
        catch (IntrospectionException e)
        {
            LOG.error("将javaBean 转成XML文件时发生内省异常", e);
        }
        finally
        {
            try
            {
                if (beanWriter != null)
                {
                    beanWriter.close();
                    beanWriter = null;
                }
            }
            catch (IOException e)
            {
                LOG.error("转成XML文件时,关闭beanWriter失败",e);
            }
        }
        return file;
    }
  
  
 /**
     * 将XML文件转成javaBean
     *
     * @param xmlFile
     * XML文件
     * @param clazz
     * 生成javaBean Class对象
     * @return Object javaBean对象
     */
    public static Object convertXML2Bean(File xmlFile, Class<?> clazz)
    {
        Object bean = null;
        BeanReader beanReader = new BeanReader();
        beanReader.getXMLIntrospector().getConfiguration()
                .setAttributesForPrimitives(false);
        beanReader.getBindingConfiguration().setMapIDs(false);
        try
        {
            beanReader.registerBeanClass(clazz);
            bean = beanReader.parse(xmlFile);
        }
        catch (IntrospectionException e)
        {
            LOG.error("将XML文件转成javaBean时发生内省异常", e);
        }
        catch (IOException e)
        {
            LOG.error("将XML文件转成javaBean时发生IO异常", e);
        }
        catch (SAXException e)
        {
            LOG.error("将XML文件转成javaBean时发生SAX解析异常", e);
        }
        return bean;
    }

将一组文件压缩成压缩文件

本文是用Java语言实现,使用的是java的类库。

实现的代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
     * 把一组文件压缩到指定的文件
     *
     * @param files
     * 文件组
     * @param zipFileName
     * 要压缩成的文件名(包括路径)
     * @param sPath
     * ZIP中的路径
     * @return 压缩文件
     */
    public static File compressFiles(File[] files,
String zipFileName,String sPath)
    {
        String sUploadPath = "test";
        File zipFile = new File(zipFileName);
        FileOutputStream fout = null;
        ZipOutputStream zipOut = null;
        ZipInputStream zipIn = null;
        FileInputStream fin = null;
        byte[] buff = new byte[1024];
        int rb;
        try
        {
            if (zipFile.exists())
            {
                File fileBak =
                 new File(zipFile.getAbsolutePath() + ".bak");
                if (zipFile.renameTo(fileBak))
                {
                    logger.debug("重命名文件成功:" + fileBak.getName());
                }
                fin = new FileInputStream(fileBak);
                zipIn = new ZipInputStream(fin);
                fout = new FileOutputStream(zipFile);
                zipOut = new ZipOutputStream(fout);
                ZipEntry ze = null;
                try
                {
                    while ((ze = zipIn.getNextEntry()) != null)
                    {
                        zipOut.putNextEntry(ze);
                        while ((rb = zipIn.read(buff)) != -1)
                        {
                            zipOut.write(buff, 0, rb);
                        }
                    }
                }
                catch (Exception e)
                {
                    logger.error("aa", e);
                }
                finally
                {
                    safeCloseInputStream(fin);
                    safeCloseInputStream(zipIn);
                    safeDeleteFile(fileBak);
                }
            }
            else
            {
                fout = new FileOutputStream(zipFile);
                zipOut = new ZipOutputStream(fout);
            }
            ZipEntry ze = null;
            for (int i = 0; i < files.length; i++)
            {
                if (files[i] != null)
                {
                    try
                    {
                        if (!files[i].exists())
                        {
                            logger.error(
                             files[i].getAbsolutePath() + "不存在");
                            continue;
                        }
                        fin = new FileInputStream(files[i]);
                        if (StringUtils.isBlank(sPath))
                        {
                            ze = new ZipEntry(files[i].getName());
                        }
                        else
                        {
                         StringBuffer sbZipEntry = new StringBuffer();
                         sbZipEntry
                             .append(sPath)
                             .append(File.separator)
                             .append(files[i].getAbsolutePath()
                             .substring(sUploadPath.length()));
                         ze = new ZipEntry(sbZipEntry.toString());
                        }
                        zipOut.putNextEntry(ze);
                        while ((rb = fin.read(buff)) != -1)
                        {
                            zipOut.write(buff, 0, rb);
                        }
                    }
                    finally
                    {
                        safeCloseInputStream(fin);
                    }
                }
            }
            logger.info("文件压缩成功:" + zipFile.getName());
            return zipFile;
        }
        catch (IOException e)
        {
            logger.error(e);
            safeDeleteFile(zipFile);
            return null;
        }
        finally
        {
            safeCloseOutStream(zipOut);
            safeCloseOutStream(fout);
        }
    }

 

用JAVA基本类库去解析HTML

这几天参加公司的定级考试,有个上机题是

访问URL: http://www.weather.com.cn/weather/101010100.shtml 页面,提取出页面中的天气信息,然后把信息按照要求输出到控制台。开始想到的是先把html文件存到本地,然后在逐行用正则表达式去解析,后来想想这种方法太土,而且解析起来会很复杂,所以就想用SAX去解析,试了一下,程序执行起来太慢,半天出不了结果,而且网络中的html标签不规则,标签不一定都有结尾,所以解析时会报错。后来在网上搜了一下,原来javax.swing.text.html包中已经提供了解析html标签的类库,在网上参考的别人的博客,地址如下:

http://blog.csdn.net/thamsyangsw/article/details/4389900

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
package com.thunisoft.kms.java.lvl2.exam;
    
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
    
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLEditorKit.ParserCallback;
import javax.swing.text.html.parser.ParserDelegator;
    
/**
 * Title: <br>
 * Description: <br>
 * Copyright: Copyright (c) 2007<br>
 * Company:<br>
 *
 * @author keep at it
 * @version 1.0
 * @date 2013-12-4
 */
public class GrapWeatherInfo extends ParserCallback
{
    /** 是否是table标签 */
    protected boolean isTable = false;
    /** 是否是a标签 */
    protected boolean isAlink = false;
    /** 是否是div标签 */
    protected boolean isDiv = false;
    /** 是否是td标签 */
    protected boolean isTd = false;
    /** 放符合条件的元素 */
    protected static Vector<String> element = new Vector<String>();
    protected static String paragraphText = new String();
    /** 要获取文件在网络中的URL */
    private static final String FILE_URL =
    /** 文件在本地磁盘的存储位置 */
    private static final String FILE_LOCATION = "E:/url.html";
    
    /** 构造方法 */
    public GrapWeatherInfo()
    {
    
    }
    
    /**
     * 开始解析
     *
     * @param r
     */
    private static void startParse(Reader r)
    {
        try
        {
         ParserDelegator ps = new ParserDelegator();
             // 负责每次在调用其 parse
             // 方法时启动一个新的
             // DocumentParser
            HTMLEditorKit.ParserCallback parser =
new GrapWeatherInfo();// 解析结果驱动这些回调方法。
            ps.parse(r, parser, true);
// 解析给定的流并通过解析的结果驱动给定的回调。
            Vector<String> link = element;
            String temp = "";
            for (int i = 1; i < link.size(); i++)
            {
                if (link.get(i).contains("星期"))
                {
                    temp = link.get(i);
                }
                if (link.get(i).equals(";"))
                {
                    System.out.println();
                }
                else if (!link.get(i).equals(">"))
                {
                    // Pattern p = Pattern.compile("\\s*|\t|\r|\n");
                    // Matcher m = p.matcher(link.get(i));
                    if (link.get(i).endsWith("夜间")
                      && !link.get(i - 1).contains("星期"))
                    {
                        System.out.println();
                        System.out.print(temp + "   ");
                        System.out.print(link.get(i) + "   ");
                    }
                    else
                    {
                        System.out.print(link.get(i) + "   ");
                    }
                }
            }
    
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    
    /**
     * 处理文本
     *
     * @param data
     * @param pos
     */
    public void handleText(char[] data, int pos)
    {
        Pattern p = Pattern.compile("\\s*|\t|\r|\n");
        Matcher m = null;
        if (isAlink)
        {
            String tempParagraphText = new String(data);
            m = p.matcher(tempParagraphText);
            if (paragraphText != null)
            {
             // 符合条件的添加到集合中去
              element.addElement(m.replaceAll(""));
            }
        }
        else if (isTd)
        {
            String tempParagraphText = new String(data);
            m = p.matcher(tempParagraphText);
            if (paragraphText != null)
            {
                // 符合条件的添加到集合中去
                element.addElement(m.replaceAll(""));
            }
        }
    }
    
    /**
     * 处理开始标签
     *
     * @param t
     * @param a
     * @param pos
     */
    public void handleStartTag(HTML.Tag t,
                       MutableAttributeSet a, int pos)
    {
        // System.out.println("start: "+t+"  "
            +a.getAttribute(HTML.Attribute.ID)+"  "
            +a.getAttribute(HTML.Attribute.CLASS));
        // 如果是<div/>
        if (t == HTML.Tag.DIV)
        {
            // 7d 是要解析的div的id属性,用来和其他的div区分
            if ("7d".equals(a.getAttribute(HTML.Attribute.ID)))
            {
                // 说明是要找的div
                isDiv = true;
            }
        }
        // 如果是<table/>
        if (t == HTML.Tag.TABLE)
        {
            // yuBaoTable 是要解析的table的class属性,
            //用来和其他的table区分
            if ("yuBaoTable".equals(
                  a.getAttribute(HTML.Attribute.CLASS)))
            {
                // 说明是要找的table
                isTable = true;
            }
        }
        // 如果是<a/>,加上是id=7d的限制
        if (t == HTML.Tag.A && isDiv)
        {
    
            if (a.getAttribute(HTML.Attribute.ID) == null)
            {
                if (a.getAttribute(HTML.Attribute.HREF) != null ?
                  a.getAttribute(HTML.Attribute.HREF).toString()
                        .endsWith(".php") : false)
                {
                    // 说明是要找的<a/>
                    isAlink = true;
                }
    
            }
        }
        if (t == HTML.Tag.TD && isDiv)
        {
            isTd = true;
        }
    }
    
    /**
     * 解析出问题时的处理方法
     *
     * @param errorMsg
     * @param pos
     */
    public void handleError(String errorMsg, int pos)
    {
    }
    
    /**
     * 处理普通tag
     *
     * @param t
     * @param a
     * @param pos
     */
    public void handleSimpleTag(HTML.Tag t,
                          MutableAttributeSet a, int pos)
    {
        handleStartTag(t, a, pos);
    }
    
    /**
     * getter method
     *
     * @return
     */
    public static String getParagraphText()
    {
        return paragraphText;
    }
    
    /**
     * 处理注释
     *
     * @param data
     * @param pos
     */
    public void handleComment(char[] data, int pos)
    {
    }
    
    /**
     * 处理end tag
     *
     * @param t
     * @param pos
     */
    public void handleEndTag(HTML.Tag t, int pos)
    {
        // System.out.println("end: "+t+"  "+pos);
        // 如果是<a/>标签
        if (t == HTML.Tag.A)
        {
            if (isAlink)
            {
                isAlink = false;
            }
        }// 如果是<table/>标签
        else if (t == HTML.Tag.TABLE && isAlink == false)
        {
            if (isTable)
            {
                isTable = false;
                // 一个table标签解析完的时候,element中加入一个;
                 //元素用来分隔每个table中的文本,方便输出
            }
            element.addElement(new String(";"));
        }// 如果是<div/>标签
        else if (t == HTML.Tag.DIV && isTable == false)
        {
            if (isDiv == true && isTable == false)
            {
                isDiv = false;
            }
        }
        else if (t == HTML.Tag.TD)
        {
            isTd = false;
        }
    }
    
    /**
     * 程序的入口
     *
     * @param args
     */
    public static void main(String args[])
    {
        InputStream input = null;
        FileOutputStream fos = null;
        BufferedReader brd = null;
        try
        {
            // 设置要提取的文件的URL
            URL url = new URL(FILE_URL);
            // 建立连接
            URLConnection conn = url.openConnection();
            conn.connect();
            // 获取输入流
            input = conn.getInputStream();
            // new 一个具体的文件输出流
            fos = new FileOutputStream(FILE_LOCATION);
            byte[] b = new byte[1024];
            int read = 0;
            // 输出
            while ((read = input.read(b)) != -1)
            {
                fos.write(b, 0, read);
            }
            // 获取HTML文件流,以UTF-8编码
            brd = new BufferedReader(
                     new InputStreamReader(
                     new FileInputStream(
                          FILE_LOCATION), "UTF-8"));
            // 开始解析HTML
            startParse(brd);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            // 关闭资源
            if (input != null)
            {
                try
                {
                    input.close();
                }
                catch (IOException e)
                {
                    input = null;
                }
            }
    
            if (fos != null)
            {
                try
                {
                    fos.close();
                }
                catch (IOException e)
                {
                    fos = null;
                }
            }
    
            if (brd != null)
            {
                try
                {
                    brd.close();
                }
                catch (IOException e)
                {
                    brd = null;
                }
            }
        }
    }
}