JVM基于分代的垃圾收集器

lpkde 9年前

JVM发展到今天,垃圾回收器已经有很多种,像标记-清除,标记-压缩,复制等,各有各的优缺点。在这里主要将其中的一种,基于分代的垃圾收集器。

基于分代的垃圾收集器的算法设计思路是:把对象按照寿命长短来分组,新创建的被划分在年轻代,经过几次回收后仍然存活的划分在年老代。这样减少了每次垃圾收集时所要扫描的对象的数量,从而提高了垃圾回收效率

JVM将Java堆分为两个区域,Young区和Old区。Young区又分为Eden,Survivor From,Survivor To三个子区;其中所有的新创建对象都在Eden区;当Eden区满后会触发minor GC,将Eden区仍然存活的对象复制到其中的一个Survivor中,另一个Survivor区中存活的对象也复制到这个Survivor中,以保证始终有一个Survivor区是空的。Old区存放的是Young区的Survivor满后触发minor GC后仍然存活的对象。当Eden区满时,会将对象存放到Survivor区中,如果Survivor去仍然存不下这些对象,GC收集器会将这些对象直接存放到Old区,如果Old区也满了,则触发Full GC,回收整个堆内存。在Sun的JVM中提供了visualvm(bin目录下的jvisualvm.exe)工具,其中有一个Visual GC插件可以看到JVM的不同代垃圾回收情况。
(Sun对堆中的不同代大小划分建议是,Young区为整个堆的1/4;而Young区中Eden:Survivor=8:2(from,to两个区1:1))

对于分代垃圾回收器,主要有三种:Serial Collector,Parallel Collector,CMS Collector。

Serial Collector(串行回收器)是JVM在client模式下默认的GC模式。可以通过JVM配置参数 -XX:+UseSerialGC 来指定GC使用该收集算法。由于是单线程GC工作,所以无论Minor GC还是Full GC,都会造成应用程序的全部停止。在Full GC中,会对整个Old区进行压缩。
在创建新对象时,如果新对象的大小超过Eden区的总大小,或者超过了PretenureSizeThreshold配置参数配置的大小,就只能在Old区分配。
当 Eden空间不足时,首先检查每次Minor GC时复制到Old区的平均对象大小是否大于Old的剩余空间,如果大于,则直接触发Full GC,否则,再看HandlePromotionFailure(-XX:-HandlePromotionFailure)的值,如果为true,则触发Minor GC,否则,触发Minor GC后再触发Full GC。也就是说,如果每次需要复制的对象的大小超过了Old的剩余空间大小,就说明当前Old区的剩余空间大小已经不能满足Minor GC时从Eden,Survivor区复制过来的对象的存放空间了,所以只能触发Full GC。
我们知道,在Minor GC中,除了回收Eden去的非活动对象外,还会把一些“老对象”复制到Old区,而“老对象”的定义可以通过配置参数 MaxTenuringThreshold来设置,如设置 -XX:MaxTenuringThreshold=10,则如果一个对象已经经历了10次Minor GC后仍然存活在Young区,则下次Minor GC时,直接将这个对象复制到Old区。还有一种情况是,如果Minor GC时,Survivor区存放不下这些将要存放到Survivor区的对象时,也会将这些对象复制到Old区;如果Old区空间不足,则触发Full GC,Full GC会清除堆中的所有垃圾对象

Parallel Collector(并行回收器)(throughput collector):使用多线程的方式,利用多CUP来提高GC的效率,主要以到达一定的吞吐量为目标;Server模式下默认GC模式。 Parallel Collector根据Minor GC,Full GC的不同分为三种,分别是 ParNewGC,ParallelGC,ParallelOldGC
1)ParNewGC可以通过-XX:+UseParNewGC参数来指定,它的对象分配和回收策略与Serial Collector类似,只是回收的线程是多线程并行回收的。在Parallel Collector中还有一个UseAdaptiveSizePolicy配置,这个参数用来动态控制Eden,Survivor From ,Survivor To的TenuringThreshold大小的,以便控制那些对象经过多少次回收后可以直接放入Old区
2)ParallelGC 在Server模式下默认的GC模式,可以通过 -XX:UseParallelGC参数来强制指定,并行回收的线程数可以通过 -XX:ParallelGCThreads来指定,这个值有一个计算公式,如果CPU核数小于8,线程数可以和核数一样;否则,值可以设置为 3+(cpu_core5)/8。通过-Xmn来设置Young区的大小,通过SurvivorRatio参数控制Eden,Survivor From,Survivor To的大小比例,如 -XX:SurvivorRatio=8 则表示Eden:Survivor From:Survivor To=8:1:1,其默认值也是8。
当在Eden区中申请内存空间时,如果Eden区剩余空间不够,则看当前申请的空间是否大于等于Eden总空间的一半,如果大于,则直接在Old中分配,如果小于,则触发Minor GC,触发前首先检查每次Minor GC时复制到Old区的平均大小是否大于Old的剩余空间,如果大于,则再触发Full GC。此次触发GC后仍然会按照这个规则重新检查一次,如果仍然满足,Full GC会再一次触发。
当Old区不足时,会触发Full GC,如果配置了ScavengeBeforeFullGC,则在Full GC之前,会先触发Minor GC
3)ParallelOldGC 可以通过-XX:+UseParallelOldGC来设置,并行回收的线程数可以通过 -XX:ParallelGCThreads来指定,这个值有一个计算公式,如果CPU核数小于8,线程数可以和核数一样;否则,值可以设置为 3+(cpu_core
5)/8。它与ParallelGC不同之处在于,前者Full GC会清除整个堆的垃圾对象,而后者只清楚部分,并对部分空间压缩。GC时同样也会造成应用程序的全部停止。

CMS Collector(并发收集器),可以通过-XX:+UseConcMarkSweepGC来指定,并发的线程数默认为4,也可以通过ParallelCMSThreads来指定。
CMS GC和上面讨论的GC不太一样。它既不是Minor GC,也不是Full GC,而是基于二者之间的一种GC,它的触发规则是检查Old区或者Perm的使用率,当比例达到一定值时触发CMS GC,回收Old区内存空间。这个比例可以通过CMSInitiatingOccupancyFraction参数来指定,默认为92%。这个默认值是通过((100-MinHeapFreeRatio)+(double)(CMSTriggerRatioMinHeapFreeRatio)/100.0) /100.0计算出来的,其中MinHeapFreeRatio=40,CMSTriggerRatio=80。如果Perm区也使用CMS GC,可以通过-XX:+CMSClassUnloadingEnabled设置,默认值也是92%,也是通过公式((100- MinHeapFreeRatio)+(double)(CMSTriggerPermRatioMinHeapFreeRatio)/100.0)/100.0,其中MinHeapFreeRatio=40,CMSTriggerPermRatio=80
触发CMS GC时回收的只是Old区和Perm区的垃圾对象,和Minor GC,Full GC基本没有关系。在这种模式下,Minor GC的触发规则和回收规则与Serial Collector基本一致,不同之处在于此GC回收为多线程。而触发Full GC有两种情况,一种是Eden区分配失败,Minor GC后分配到Survivor区时Survivor不够,Old区也不够。另一种情况是当CMS GC正在进行时,向Old区申请内存失败。
在hotspot1.6中如果使用了这种GC方式,在程序中显示的调用System.gc(),且设置了ExplicitGCInvokesConcurrent参数,那么在使用NIO时可能会引发内存泄漏。
CMS GC何时执行JVM还会有一些时机选择,如当前CPU是否繁忙。因此它有一个计算规则,并根据这个规则来动态调整。这也给JVM带来了一些开销,如果要禁止这个动态调整,禁止CMS GC自动触发,则可以配置参数 -XX:+UseCMSInitiatingOccuoancyOnly来实现