GC与JVM堆

GC与JVM堆
引言
    这些天一直在和兄弟们做通信的模型测试,在环境中为了模拟多客户端访问,经常使用大量的线程,在线程中用到了很多对象,占据了不少存储空间,GC(garbage collector)而引发的问题浮出水面,虽然测试已经结束,有些问题还没弄清楚,于是我找了很多文章想看个究竟,现在把所看文章的精华摘抄和自己的所得写下来,扔块破砖,希望能砸出个玉矿来:)。
JVM堆相关知识
    为什么先说JVM堆?
    JVM的堆是Java对象的活动空间,程序中的类的对象从中分配空间,其存储着正在运行着的应用程序用到的所有对象。这些对象的建立方式就是那些new一类的操作,当对象无用后,是GC来负责这个无用的对象(地球人都知道)。

GC是如何去回收这些对象的,当中会有什么影响?这跟JVM堆本身的构造是分不开的。
JVM堆的结构如下:

JVM堆

    新域:存储所有新成生的对象

  
    旧域:新域中的对象,经过了一定次数的GC循环后,被移入旧域
=================================================================

永久域:存储类和方法对象,从配置的角度看,这个域是独立的,不包括在JVM堆内。默认为4M。



    GC 是通过JVM上的一个或一组进程来实现,GC在运行时同样会占用堆空间,也占用cpu,而且,GC运行时,是对JVM堆进行操作,所以,应用程序会停止运行,也就是说GC运行时,不仅会消耗一定的资源,而且会影响应用程序的运行。当应用比较小而简单时,GC的影响可以忽略,但对于大型而复杂的应用,GC的处理机制就显得非常重要了。

水是有源的,树是有根的,GC有影响也是有原因的!(王大鹏 语录)
GC浅谈
    GC的工作目的很明确:在堆中,找到已经无用的对象,并把这些对象占用的空间收回使其可以重新利用。

    Java 规范没有对GC进行明确的定义,所以不同的JVM,其GC的实现方法就不太相同,在<<Java编程思想>>那块砖头中讲述了一种方法:对活动对象和无用对象使用计数器,如果这个对象被引用一次,则其对应的计数器加1,如果对象不在作用域中使用,计数器减1,当计数器为0,这个对象就可以被回收了。
这个方法的优点是针对每个对象做操作,不会大规模地进行对堆的操作,所以不会长时间中断应用程序,缺点也明显,它必须实时运行,所以程序开销就大了,唉,鱼和熊掌不能兼得。

    大多数垃圾回收的算法思路都是一致的:把所有对象组成一个集合,或可以理解为树状结构,从树根开始找,只要可以找到的都是活动对象,如果找不到,这个对象就是凋零的昨日黄花,应该被回收了。

    从资料上查到的这种算法如下:
l    tracing算法(tracing collector): 从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象。基于tracing算法的垃圾收集也称为标记和清除 (mark-and-sweep)垃圾收集器。
l    compacting 算法(compacting collector):在清除的过程中,将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用 在新的位置能识别原来的对象。一般增加句柄和句柄表。
l    coping算法 (coping collector):该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于 coping算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
l    generation算法 (generational collector):在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代 (generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。
l    adaptive算法(adaptive collector):在特定的情况下,一些垃圾收集算法会优于其它算法。基于adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

了解了GC的算法,也熟悉了JVM堆的构造,看看两者的关系。
GC与JVM堆的关系
在sun 的文档说明中,对JVM堆的新域,是采用coping算法,新域会被分为3个部分
l    第一个部分叫Eden。  (伊甸园??可能是因为亚当和夏娃是人类最早的活动对象?)
l    另两个部分称为辅助生存空间(幼儿园),我这里一个称为A空间,一个称为B空间。

对于新生成的对象,都放在Eden中;当Eden充满时(小孩太多了),GC将开始工作,首先停止应用程序的运行,开始收集垃圾,把所有可找到的对象都复制到A空间中,一旦当A空间充满,GC就把在A空间中可找到的对象都复制到B空间中(会覆盖原有的存储对象),当B空间满的时间,GC就把在B空间中可找到的对象都复制到A空间中,AB在这个过程中互换角色,那位客官说了:拷来拷去,烦不烦啊?什么时候是头?您别急,在活动对象经过一定次数的GC操作后,这些活动对象就会被放到旧域中。对于这些活动对象,新域的幼儿园生活结束了。

新域为什么要这么折腾?
起初在这块我也很迷糊,又查了些资料,原来是这样:应用程序生成的绝大部分对象都是短命的,copying算法最理想的状态是,所有移出Eden的对象都会被收集,因为这些都是短命鬼,经过一定次数的GC后应该被收集,那么移入到旧域的对象都是长命的,这样可以防止AB空间的来回复制影响应用程序。
实际上这种理想状态是很难达到的,应用程序中不可避免地存在长命的对象,copying算法的发明者要这些对象都尽量放在新域中,以保证小范围的复制,压缩旧域的开销可比新域中的复制大得多(旧域在下面说)。

对于旧域,采用的是tracing算法的一种,称为标记-清除-压缩收集器,注意,这有一个压缩,这是个开销挺大的操作。所以,新域中采用copying算法是有道理的。
搜索到的结果与建议
    从上面的推导可以得出很多结论,下面是前辈的经验总结与自已的认识
l    JVM堆的大小决定了GC的运行时间。如果JVM堆的大小超过一定的限度,那么GC的运行时间会很长。
l    对象生存的时间越长,GC需要的回收时间也越长,影响了回收速度。
l    大多数对象都是短命的,所以,如果能让这些对象的生存期在GC的一次运行周期内,wonderful!
l    应用程序中,建立与释放对象的速度决定了垃圾收集的频率。
l    如果GC一次运行周期超过3-5秒,这会很影响应用程序的运行,如果可以,应该减少JVM堆的大小了。
l    前辈经验之谈:通常情况下,JVM堆的大小应为物理内存的80%。

在编程中应该注意到的:
l    大家都知道GC的执行时间是不确定的,调用System.gc()也无法确定GC什么时候执行,这个函数只是向JVM发一个申请,究竟什么时候做,还得老大JVM决定。
l    JVM 也不能保证Finalize方法一定会被调用,而且对finalize方法的使用要注意,GC为了支持finalize,对覆盖这个函数的对象作很多附加的工作,同时,它要让在finalize运行之后,将要释放的对象变成可访问到的(树状结构中),GC还要再检查一次这个对象是否变成可访问到的,如果一来,GC的性能就打了折扣了。另外,finalize不同于Java其它的普通方法,它是通过类似于c语言的通过分配内存来干活的,相当于在 finalize()内部调用了类似于full()的c函数。所以,资料上不推荐用finalize进行普通清除。
l    查了N多的资料,JVM的缺省选项在通常情况下是最优的。
l    尽早释放无用的对象,如果对象无用了,就将其设置为null,这就告诉GC,这个对象你可以回收了。但要注意,如果对象是很复杂的类型(树,图等),或有监听器,那就要小心了。

附录
下面的地址是讲JVM的一些选项设置,用于设置JVM堆的参数和输入GC调试信息等。
官方设置JVM的选项说明网页
http://java.sun.com/docs/hotspot/VMOptions.html
一个sun公司的人收集的JVM的选项
http://blogs.sun.com/roller/resources/watt/jvm-options-list.html


Magneto_cn
2005-9-15

你可能感兴趣的:(jvm,多线程,算法,活动,sun)