一、CMS介绍

全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器。

1、并行,STW时间短暂。
2、没有压缩和整理,产生内存碎片。

对象在标记过程中,根据标记情况,分成三类:

1、白色对象,表示自身未被标记;
2、灰色对象,表示自身被标记,但内部引用未被处理;
3、黑色对象,表示自身被标记,内部引用都被处理;

垃圾回收(2)CMS_第1张图片

触发时间:如果添加了一下参数

垃圾回收(2)CMS

当老年代的使用率达到80%时,就会触发一次cms gc。

二、CMS的收集过程分为6个步骤。

假设CMS GC之前的堆结构如下图:
垃圾回收(2)CMS_第2张图片

1、初始标记(InitialMarking)

这是一个STW过程,主要分两步
1、标记GC Roots可达的老年代对象;
2、遍历GC Roots下的新生代对象能够可达的老年代对象;
3、此过程不对以上可达的老年代对象进行进一步的可达扫描。

结果:
垃圾回收(2)CMS_第3张图片

2、并发标记(Marking)

该阶段GC线程和应用线程并发执行,遍历InitialMarking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
这个过程应用线程在运行,可能Young GC也会发生,会发生以下的情况:
1、新生代对象晋升到老年代
2、在老年代分配对象
3、新老年代对象的引用发生变化。

结果:

垃圾回收(2)CMS_第4张图片

2.1、那么如何处理并发标记过程中对象的变化呢?

CMS使用上一节讲过的Card Table来解决这个问题
并发标记过程中引用发生变化的对象所在的Card,在Card Table来记录为“脏卡”,这样在后面重新标记的时候会把这些对象也当做GC Root来遍历

但是Young GC如果发生,比方说:
1、并发标记还未扫描到脏卡1.
2、Young GC扫描完脏卡,并改变dirty到clean.
3、并发标记扫描,发现卡1已不是脏卡,则不会处理,这就造成了漏标。

2.2、如果解决以上的问题呢?

CMS中,有另一种数据结构(Mod Union Table)
Mod Union Table是一个位向量,每个单元的大小只有1位,每个单元对应一个Card(Card的大小是512字节,Card Table每一个单元的大小是1个字节)
在新生代GC处理dirty card之前,先把该card在Mod Union Table里面的对应项置位。
这样,CMS在执行重新标记阶段的时候,就会扫描Mod Union Table和card table里面被标记的项。

3、预清理(Precleaning&AbortablePreclean)

3.1 Precleaning

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

1、处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
2、在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(包括ModUnionTalble),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象。

3.2、AbortablePreclean

该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

为什么需要这个阶段,存在的价值是什么?

因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

在该阶段,主要循环的做两件事:

1、处理 From 和 To 区的对象,标记可达的老年代对象
2、和上一个阶段一样,扫描处理Dirty Card和ModUnionTalble中的对象。

当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:

1、可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。
2、如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
3、如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。

4、重新标记(STW的过程)

在之前的并行阶段,可能产生新的引用关系如下:

1、老年代的新对象被GC Roots引用
2、老年代的未标记对象被新生代对象引用
3、老年代已标记的对象增加新引用指向老年代其它对象
4、新生代对象指向老年代引用被删除

上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以需要做以下事情:

1、遍历新生代对象,重新标记
2、根据GC Roots,重新标记
3、遍历老年代的Dirty Card和Mod Union Table,重新标记

在第1步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在这之前发生一次YGC,这样就可以避免扫描无效的对象。

CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。

5、并发清理

清理在标记阶段收集标识为不可达的对象

6、重置

清除数据结构,准备下一次并发收集。