个人成长博客

纸上得来终觉浅,绝知此事要躬行

0%

CMS垃圾收集器

CMS垃圾收集器

CMS垃圾回收器的全称是Concurrent Mark-Sweep Collector,从名字上可以看出两点,一个是使用的是并发收集,第二个是使用的收集算法是标记清除算法Mark-Sweep。该收集器的特点是低延迟低停顿,不过因为采用的是标记清除算法所以会产生浮动垃圾的问题。CMS收集器是为了低延迟低停顿而生,通过尽可能的并行执行垃圾回收的几个阶段来把延迟控制到最低。

主要步骤

CMS 处理过程有七个步骤:

  1. 初始标记(CMS-initial-mark) ,会导致stw;
  2. 并发标记(CMS-concurrent-mark),并发追溯标记,与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),查找执行并发阶段从年轻代晋升到老年代的对象,与用户线程同时运行;
  4. 可中断预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
  5. 重新标记(CMS-remark) ,暂停虚拟机,扫描CMS堆中剩余的对象,会导致swt;
  6. 并发清除(CMS-concurrent-sweep),清理垃圾对象,与用户线程同时运行;
  7. 并发重置(CMS-concurrent-reset),等待下次CMS的触发,与用户线程同时运行;

初始标记

初始标记阶段主要做两件事:

  1. 一是遍历GCRoot可直达的老年代对象;
  2. 二是遍历新生代直达的老年代对象。(指的是年轻代中还存活的引用类型对象,引用指向老年代中的对象)。

如图:其中1为GCRoot可直达的老年代对象,2和3为新生代引用的老年代对象

初始标记阶段是完全STW的,引用程序会暂停。通过-XX:+CMSParallelInitialMarkEnabled参数可以开启该阶段的并行标记,使用多个线程进行标记,减少暂停时间,线程数不要超过cpu的核数。

并发标记

并发标记阶段是与应用程序一起执行的,这个阶段主要做两件事:

  1. 从“初始标记”阶段标记的对象开始找出所有存活的对象;

    如图:根据1、2、3发现4和5

  2. 将在并发阶段新生代晋升到老年代的对象、直接在老年代分配的对象以及老年代引用关系发生变化的对象所在的card标记为dirty,避免在重新标记阶段扫描整个老年代。

    如图:在并发标记阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;

因为并发标记阶段与引用程序一起执行,在标记阶段会使用三色标记算法。三色标记法把 GC 中的对象划分成三种情况:

  1. 白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
  2. 灰色:正在搜索的对象
  3. 黑色:搜索完成的对象(不会当成垃圾对象,不会被 GC)

预清理

通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情:

  1. 并发标记阶段在Eden分配了对象A,并且A引用了老年代对象B,那么这个阶段标记B为活跃对象。
  2. 扫描并发标记阶段的Dirty Card,重新标记那些在并发标记阶段引用被更新的对象

如图:根据dirty card3,扫描出对象6

可中断预清理

该阶段存在的目的是减轻重新标记的工作量,减少暂停时间,主要做两件事情:

  1. 这个阶段主要处理from和to区域对象引用老年代的对象的变化
  2. 同样也会继续处理dirty card的对象引用

该阶段退出的条件有三个:

  1. 这个阶段默认设置(CMSMaxAbortablePrecleanTime)的时间是5s,如果执行逻辑超过5s,会自动终止这个阶段
  2. 或者当eden区使用内存值小于CMSScheduleRemarkEdenPenetration,默认50%时,也会退出这个阶段
  3. 扫描次数达到CMSMaxAbortablePrecleanLoops(默认是0,不退出)

该阶段是希望能发生一次Young GC,这样就可以新生代对象的数量,降低重新标记的工作量,因为重新标记会扫描整个新生代

重新标记

这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个老年代的所有的存活对象。这个阶段,重新标记的内存范围是整个堆,包含young_gen和old_gen。主要从三个部分着手标记:

  1. 遍历young_gen区
  2. 遍历在预清理阶段剩余的dirty card
  3. 遍历GC Roots

为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代。

由于该阶段遍历的区域很多,因此有可能会耗时比较长,并且该阶段是完全的STW的。可以通过CMSScavengeBeforeRemark参数可以强制在重新标记阶段之前强制进行一次YoungGC,通过设置CMSParallelRemarkEnabled参数可以开启并行的Remark,加快remark的速度。

并发清理

移除那些不用的对象,回收它们占用的空间并且为将来使用。该阶段有可能产生浮动垃圾,可以通过参数UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction来控制压缩次数。

并发重置

该阶段是最后一个阶段,重置CMS的数据结构。

CMS失败处理

并发模式失败日志(concurrent mode failure)

当老年代无法容纳新生代GC晋升的对象时发生并发模式失败,并发模式失败意味着CMS退化成完全STW的Full GC,也就是Serial GC。

针对这种情况,有两个方面需要考虑:

  1. 给后台线程更多的运行机会。也就是说更早的启动并发收集周期。CMS收集器在老年代使用占到60%的时候启动比占到70%才启动,显然前者完成垃圾回收的几率更大。为了实现这种配置,可以同时设置以下两个参数:-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly。如果同时设置了这两个参数就可以让CMS只根据老年代的使用比例来决定是否启动CMS垃圾收集。
  2. 更多的线程来运行CMS。之所以出现并发模式失败,是因为CMS的速度跑不赢对象晋升到老年代的速度了。所以可以通过给CMS更多的线程来加快CMS的速度。可以通过-XX:ConGCThreads=N来设置后台线程的数量。默认情况下线程数ConcGCThreads=(3+ParallelGCThreads)/4,是根据ParallelGCThreads来计算的,ParallelGCThreads的值可以通过-XX:ParallelGCThreads参数来设置。并不是说设置越多的线程来运行CMS越好,因为CMS在运行的时候会完整的占用一颗CPU,所以在CPU比较紧张的情况下,这个值还是要谨慎设置的。

晋升失败(promoration failure)

老年代有足够的空间,但是由于碎片化严重,无法容纳新生代中晋升的对象,发生晋升失败。

晋升失败的原因是碎片化严重,所以这个问题的解决方案就是如何减少碎片化的问题。CMS提供了两个参数来对碎片进行整理和压缩。-XX:+UseCMSCompactAtFullCollection这个设置的作用是在进行FullGC的时候对碎片进行整理和压缩。-XX:CMSFullGCsBeforeCompaction=*这个参数是设置在进行多少次FullGC的时候对老年代的内存进行一次碎片整理压缩。通过设置这两个参数可以有效的对碎片问题进行优化。同样需要注意的是对碎片进行整理压缩是一个比较耗时的操作,所以也需要谨慎设置。