JVM之垃圾回收机制

一. 收集器

JVM通过GC来回收堆和方法区中的内存,通常采用收集器的方式实现GC,主要的收集器有引用计数收集器和跟踪收集器。

1、 引用计数收集器

引用计数收集器采用分散式的管理方式,通过计数器记录对象是否被引用。

引用计数收集器需要在每次对象赋值时进行引用计数器的增减,有一定消耗;此外,引用计数收集器对于循环引用的场景无法实现回收。因此,Sun JDK在实现GC时未采用此种方式。

2、 跟踪计数器

跟踪计数器采用集中式的管理方式,全局记录数据的引用状态。主要有复制(Copying)、标记-清除(Mark-Sweep)和标记-压缩(Mark-Compact)三种实现算法:

    1) 复制(Copying)

    复制从根集合中扫描出存活的对象,并将找到的存活对象复制到一块新的未完全使用的空间中。

   2) 标记-清除(Mark-Sweep)

    标记-清除从根集合开始扫描,对存活的对象进行标记,扫描完毕后,再扫描整个空间中未标记的对象,并进行回收。

    3) 标记-压缩(Mark-Compact)

    标记-压缩采用和标记-清除一样的方式对存活对象进行标记,但在清除时有所不同。在回收不存活对象所占用的内存空间后,会将其他所有存活对象都往左端空闲的空间进行移动,并更新引用其对象的指针。

二、Sun JDK中可用的GC

Sun JDK中可用的GC方式如下图所示:

untitled

1、新生代可用GC

Sun JDK基于复制(Copying)算法来实现对新生代对象的回收。

Eden Space存放新创建的对象,S0或S1的其中一块用于存放在Minor GC触发时作为复制的目标空间,当其中一块为复制的目标空间时,另一块内容则被清空。

   1)串行GC(Serial GC)

    采用串行GC时,SurvivorRatio的值对应EdenSpace/Survivor Space,默认为8。新生代内存分配采用空闲指针(bump-the-pointer)的方式,指针保持最后一个分配对象在新生代内存区间的位置,当有新的对象要分配内存时,只须检查剩余的空间是否能够存放新的对象,够则更新指针,并创建对象,不够则触发Minor GC。

当扫描存活对象时,Minor GC所做的动作为将存活的对象复制到目前作为To Space的S0或S1中;当再次进行Minor GC时,之前作为To Space的S0或S1则转换为From Space,通常存活的对象在Minor GC后并不是直接进入旧生代,只有经历过几次Minor GC仍然存活的对象,才放入旧生代中,这个在Minor GC中存活的最大次数在串行和ParNew方式时可通过-XX:MaxTenuringThreshold来设置,但并不是代表对象一定会存活MaxTenuringThreshold次才会晋升到旧生代。串行和ParNew采用一个规则在每次Minor GC后计算可存活的次数,规则为累积每个age的对象所占的内存,一直计算到占用大小超过Survivor Space一半的age;如果计算了所有age均为超过,则以MaxTenuringThreshold为准,否则,以age为准。在Parallel Scavenge时,默认情况下由HotSpot根据运行情况来决定。当To Space空间满,剩下的存活对象则直接转入旧生代。

串行GC在整个扫描和复制过程中均采用单线程方式进行,更加适用于单CPU,新生代空间较小及对暂停时间要求不是非常高的应用上,也是client级别(CPU核数小于2或物理内存小于2GB)或32位Windows机器上默认采用的GC方式,可通过-XX:UseSerialGC的方式来强制指定。

    2) 并行回收GC(Parallel Scavenge)

      采用并行回收GC时,默认情况下Eden、S0、S1通过InitialSurvivorRatio来划分比例,默认值为8,可通过-XX: InitialSurvivorRatio来调整。在Sun JDK1.6以后也可通过-XX: SurvivorRatio来调整,但会将此值+2赋给InitialSurvivorRatio。同时配置了上述两者时,以InitialSurvivorRatio为准。默认情况下,不配置时,假设-Xmn设置为16MB,则Eden Space为12MB,两个Survivor Space各为2MB;如果配置SurvivorRatio为8,那么Eden Space为12.8MB,两个Survivor Space各为1.6MB。

并行回收GC在启动时Eden、S0和S1的比例按照上述方式分配,但在运行一段时间后并行GC会根据Minor GC的频率、消耗时间等来动态调整Eden、S0和S1的大小。可通过-XX:UseAdaptiveSizePolicy来固定Eden、S0和S1大小。

并行回收GC不根据-XX:PretenureSizeThreshold来决定是否在旧生代上直接分配对象,而是当需要给对象分配内存时,在Eden Space空间不足的情况下,如果此对象的大小大于等于Eden Space的一半,则直接在旧生代上分配。

并行回收GC也采用复制(Copying)算法,但其在扫描和复制时均采用多线程方式进行。

并行回收GC适用于多CPU、对暂停时间要求较短的应用。是server级别(CPU核数超过2且物理内存超过2G)的机器(32位windows机器除外)上默认采用的GC方式,也可同-XX:+UseParallelGC来强制指定。并行方式默认线程数根据CPU核数计算,当CPU核数小于等于8,并行的线程数为CPU核数;当CPU核数多于8,则为3+(CPU核数×5)/8,也可采用-XX:ParallelGCThreads来强制指定线程数。

     3) 并行GC(ParNew)

      默认情况下,并行GC在基于SurvivorRatio值划分Eden Space和两块Survivor Space的方式上与串行GC一样,在开启-XX:UseAdaptiveSizePolicy后,则在每次Minor GC后动态计算Eden、to的大小。

并行GC和并行回收GC的区别在于并行GC需配合旧生代使用CMS GC。CMS GC在进行旧生代GC时,有些过程是并发进行的,如此时发生Minor GC,则需要进行相应的处理。因此,并行GC不可以与并行的旧生代GC同时使用。(http://blogs.sun.com/jonthecollector/entry/our_collectors)

在旧生代配置为使用CMS GC的情况下,新生代默认采用并行GC的方式,也可通过-XX:+UseParNewGC来强制指定。

2、 旧生代和持久代可用的GC

JDK提供串行、并行和并发三种GC来对旧生代和持久代对象所占用内存进行回收。

  1) 串行

    串行基于Mark-Sweep-Compact实现

    串行GC分三个阶段执行:

    i. 从根集合对象开始扫描,按照三色着色的方式对对象进行标识;

    ii. 遍历整个旧生代空间或持久代空间,找出其中未标识的对象,并回收其内存;

    iii. 执行滑动压缩(Sliding Compacting),将存活对象向旧生代空间的开始处进行滑动,最终留出一块连续的到结尾处的空间。

串行执行的整个过程需暂停应用,且采用单线程方式执行,可通过增加-XX:+PrintGCApplicationStoppedTime来查看GC造成的应用暂停时间。

串行是client级别或32位windows机器上默认采用的GC方式,也可通过-XX:+UseSerialGC来强制指定。

    2) 并行Compacting

    并行采用Mark-Compact实现,在内存分配方式上和串行方式相同。

    并行大部分时间是多线程同时进行操作,对应用造成的暂停时间会缩短。但由于旧生代较大,在扫描和标识对象上需要花费较长的时间,因此仍要消耗一定的应用暂停时间。

    并行是server级别机器(非32位windows)上默认采用的GC方式,可通过-XX:+UseParallelGC来指定使用Parallel Mark Sweep,通过-XX:+UseParallelOldGC来指定使用Parallel Compacting。

    3) 并发(CMS:Concurrent Mark-Sweep GC)

    CMS GC的好处为GC的大部分动作与应用并发进行,可大大缩短GC造成应用暂停的时间。

    CMS GC执行扫描、着色和清除的步骤如下:第一次标记(Initial Marking)->并发标记(Concurrent Marking)->重新标记(Final Marking(remark))->并发收集(Concurrent Sweeping)。整个步骤中,只有Initial Marking和Final Marking需要暂停整个应用,其余步骤都与应用并发进行,所以在GC过程中影响应用时间很短。

     对比并行GC,CMS GC需要执行三次mark,所以其完整的一次GC执行时间比并行GC长,对于关注于GC总耗时时间的应用,CMS GC并不适用。

    CMS GC的回收内存方式使其容易产生内存碎片,降低内存空间利用率,可通过在JVM中指定-XX:UseCMSCompactAtFullCollection来保证每次执行Full GC时都会进行碎片整理;也可通过-XX:CMSFullGCBeforeCompaction来指定在进行一定次数的Full GC后进行碎片整理。碎片整理需要暂停整个应用

   CMS在回收时容易产生一些应该回收但是要等到下次CMS才能被回收的对象(通常称为“浮动垃圾”),再加上CMS回收过程中大部分时间是和应用并发进行,要求采用CMS GC的情况下需要提供更多的可用旧生代空间

   CMS GC默认不开启,可通过-XX:+UseConcMarkSweepGC来启用。默认开启线程数为(并行GC线程数+3)/4,可通过-XX:ParallelCMSThreads来强制指定。

    CMS GC的触发条件一种为旧生代已使用空间达到设定的CMSInitiatingOccupancyFraction百分比或持久代已使用空间达到设定的CMSInitiationPermOccupancyFraction百分比;另一种为JVM自动触发,JVM根据之前的GC频率及旧生代的增长趋势来决定何时进行CMS GC,如果不希望JVM自行触发,可设置-XX:UseCMSInitiatingOccupancyOnly=true。

    持久代GC也可采用CMS方式,可设置-XX:+CMSClassUnloadingEnabled

    CMS GC 日志信息参见:http://www.sun.com/bigadmin/content/submitted/cms_gc_logs.jsp

3、Full GC

Full GC对新生代、旧生代和持久代都进行GC。当Full GC触发时,首先按照新生代配置的GC方式对新生代进行GC(在新生代采用PS GC时,可通过-XX:ScavengeBeforeFullGC来禁止Full GC时对新生代进行GC),然后按照旧生代GC方式对旧生代、持久代进行GC。如果在进行Minor GC前,Minor GC后移到旧生代的对象多余旧生代的剩余空间,这时Minor GC不会执行,而直接采用旧生代的GC方式对新生代、旧生代和持久代进行回收。

除直接调用system.gc外,触发Full GC执行的情况有以下四种:

  1) 旧生代不足

   只有在从新生代转入大对象、大数组时才会出现此种现象。当执行Full GC后空间仍然不足,则抛出java.lang.OutOfMemoryError:Java heap space。

   为避免此现象,调优时应尽量让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间、尽量不要创建大对象和数组。

  2) Permanet Generation空间满

   在系统中要加载的类、反射的类和调用的方法较多时可能出现此现象。当执行Full GC后空间仍然不足,则抛出java.lang.OutOfMemoryError:PermGen Space。

   为避免此现象,可采用的方法为增大Perm Gen空间或采用CMS GC。

   3) CMS GC时出现promotion failed和cocurrent mode failure

    promotion failed由于在进行Minor GC时,Survivor Space放不下,对象只能放入旧生代,而此时旧生代也放不下而造成的;cocurrent mode failure由于在执行CMS GC过程中,同时有对象要放入旧生代,而此时旧生代空间不足造成的。

    应对措施为:增大Survivor Space、旧生代空间或调低触发CMS GC的比率。

   4) 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间

   HotSpot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时做了一个判断,如果之前统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么直接触发Full GC。

  除上述4种情况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可在启动时通过-Java –Dsun.rmi.dgc.client.gcInterval来设置Full GC执行的时间间隔或通过-XX:+DisabledExplicitGC来禁止RMI调用System.gc。

4、 小结

Client和server模式的默认GC方式:

             新生代GC方式                        旧生代和持久代GC方式

Client       串行GC                              串行GC

Server       并行回收GC                          Parallel Mark Sweep GC

Sun JDK GC的组合方式:

                                    新生代GC方式           旧生代和持久代GC方式

-XX:+UseSerailGC                    串行GC                 串行GC

-XX:+UseParallelGC                  并行回收GC             Parallel Mark Sweep GC

-XX:+UseConcMarkSweepGC             并行GC                 并发GC

                                                  当出现concurrent mode failure时采串行GC

-XX:+UseParNewGC                    并行GC                 串行GC

-XX:+UseParallelOldGC               并行回收GC             Parallel Compacting GC

-XX:+UseConcMarkSweepGC             串行GC                 并发GC

-XX:+UseParNewGC                    当出现concurrent mode failure或promotion failde时采用串行GC

不支持的组合方式
1.-XX:+UseParNewGC -XX:+UseParallelOldGC

2.-XX:+UseParNewGC -XX:+UseSerailGC

两种选择GC的方式:

1) 吞吐量优先

吞吐量优先指GC所消耗的时间占总运行时间的百分比。

吞吐量优先的策略即为以吞吐量为指标,由JVM自行选择相应的GC策略及控制新生代、旧生代的内存大小,可通过-XX:GCTimeRatio来指定此策略。

2) 暂停时间优先

暂停时间指每次GC造成的应用的停顿时间。

暂停时间优先的策略以暂停时间为指标,由JVM自行选择相应的GC策略及控制新生代、旧生代的内存大小,可通过-XX:MaxGCPauseMillis来指定此策略。

对于以上两种参数都指定的情况,首先满足暂停时间优先策略,再满足吞吐量优先策略。

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

*