CMS垃圾回收机制
CMS垃圾回收机制
简介
Concurrent Mark Sweep
,是一款基于并发、使用标记清除算法的垃圾回收算法,只针对老年代进行垃圾回收。CMS收集器工作时,GC工作线程和用户线程可以并发执行,以达到降低STW
时间的目的。
开起VM选项-XX:+UseConcMarkSweepGC
,表示对老年代的回收采用CMS。
生产环境中常用的两种垃圾收集器(ParNew:年轻代,CMS:老年代)
CMS维度
根据GC的触发机制分为:周期性Old GC(被动)和主动Old GC
一般都是被动GC,这里主要说的也是这个。
主动Old GC的过程,触发条件比较苛刻:
- YGC过程发生Promotion Failed,进而对老年代进行回收
- 比如执行了
System.gc()
,前提是没有参数ExplicitGCInvokesConcurrent
如果触发了主动Old GC
,这时周期性Old GC
正在执行,那么会夺过周期性Old GC
的执行权(同一个时刻只能有一种在Old GC
在运行),并记录 concurrent mode failure
或者 concurrent mode interrupted
。
CMS收集器特点
- 尽可能降低停顿
- 会影响系统整体吞吐量和性能。比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半。
- 清理不彻底。因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理。
- 不能在空间快满时再清理,因为和用户线程一起运行。-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值,如果不幸内存预留空间不够,就会引起concurrent mode failure。
STW
首先,我们需要厘清一个概念,即只有标记
阶段才需要STW (Stop The World)
。标记完成以后,需要清除的对象已经确定,无论此时是否产生新的垃圾,都不影响对这些对象的清理。也就是说,清除
阶段是可以设计成和用户线程并发执行的。
JVM在暂停的时候,需要选准一个时机,由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点(safepoint)
的概念:程序只有在运行到安全点的时候,才可以暂停下来。HotSpot
采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起。HotSpot
使用了几条短小精炼的汇编指令便可完成安全点轮询以及触发线程中断,因此对系统性能的影响几乎可以忽略不计。
可达性
可达性
是指,如果一个对象会被至少一个程序中的可达对象通过直接或间接的方式引用,则称该对象是可达的
。更详细地说,一个对象满足一下两个条件之一,即被判定为可达的。
1.本身是根对象。根(root)是指由堆以外空间访问的对象。JVM会将以下对象标记为根:a.虚拟机栈(栈帧中的本地变量表)中引用的对象;b.方法区中的类静态属性引用的对象;c.方法区中的常量引用的对象;d.本地方法栈中JNI的引用对象。
2.被一个可达的对象引用。
CMS的正常过程
这里我们首先看下CMS并发收集周期正常完成的几个状态。
初始标记(CMS-initial-mark)
这个阶段是标记从GcRoots直接可达的老年代对象、新生代引用的老年代对象,就是下图中灰色的点。这个过程是单线程的(JDK7之前单线程,JDK8之后并行,可以通过参数CMSParallelInitialMarkEnabled调整)。
并发标记(CMS-concurrent-mark)
由上一个阶段标记过的对象,开始tracing过程,标记所有可达的对象,这个阶段垃圾回收线程和应用线程同时运行,如上图中的灰色的点。在并发标记过程中,应用线程还在跑,因此会导致有些对象会从新生代晋升到老年代、有些老年代的对象引用会被改变、有些对象会直接分配到老年代,这些受到影响的老年代对象所在的card会被标记为dirty,用于重新标记阶段扫描。这个阶段过程中,老年代对象的card被标记为dirty的可能原因,就是下图中绿色的线:
预清理(CMS-concurrent-preclean)
预清理,也是用于标记老年代存活的对象,目的是为了让重新标记阶段的STW尽可能短。这个阶段的目标是在并发标记阶段被应用线程影响到的老年代对象,包括:(1)老年代中card为dirty的对象;(2)幸存区(from和to)中引用的老年代对象。因此,这个阶段也需要扫描新生代+老年代。【PS:会不会扫描Eden区的对象,我看源代码猜测是没有,还需要继续求证】
可中断预清理(CMS-concurrent-abortable-preclean)
这个阶段的目标跟“预清理”阶段相同,也是为了减轻重新标记阶段的工作量。可中断预清理的价值:在进入重新标记阶段之前尽量等到一个Minor GC,尽量缩短重新标记阶段的停顿时间。另外可中断预清理会在Eden达到50%的时候开始,这时候离下一次minor gc还有半程的时间,这个还有另一个意义,即避免短时间内连着的两个停顿,
在预清理步骤后,如果满足下面两个条件,就不会开启可中断的预清理,直接进入重新标记阶段:
-
Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这个参数的默认值是2M;
-
Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这个参数的默认值是50%。
如果不满足上面两个条件,则进入可中断的预清理,可中断预清理可能会执行多次,那么退出这个阶段的出口有两个(源码参见下图):
-
设置了CMSMaxAbortablePrecleanLoops,并且执行的次数超过了这个值,这个参数的默认值是0;
-
CMSMaxAbortablePrecleanTime,执行可中断预清理的时间超过了这个值,这个参数的默认值是5000毫秒。如果是因为这个原因退出,gc日志打印如下:
有可能可中断预清理过程中一直没等到Minor gc,这时候进入重新标记阶段的话,新生代还有很多活着的对象,就回导致STW变长,因此CMS还提供了CMSScavengeBeforeRemark参数,可以在进入重新标记之前强制进行依次Minor gc。
重新标记(CMS-concurrent-mark)
重新扫描堆中的对象,进行可达性分析,标记活着的对象。这个阶段扫描的目标是:新生代的对象 + Gc Roots + 前面被标记为dirty的card对应的老年代对象。如果预清理的工作没做好,这一步扫描新生代的时候就会花很多时间,导致这个阶段的停顿时间过长。这个过程是多线程的。
并发清除(CMS-concurrent-sweep)
用户线程被重新激活,同时将那些未被标记为存活的对象标记为不可达;
并发重置 (CMS-concurrent-reset)
CMS内部重置回收器状态,准备进入下一个并发回收周期。
CMS的几个阶段(网上的第二种说法,虽然感觉上面一种比较正确)
CMS
将可达性分析分解成两个阶段:a.仅扫描与根节点直接关联的对象; b.继续向下扫描完所有对象。因此,标记
阶段也被拆分成两个阶段,即初始标记
和并发标记
。
CMS完整的收集过程如下:
-
初始标记(init-mark)
:仅扫描与根节点直接关联的对象并标记,这个阶段必须STW
, 由于跟节点数量有限,所以这个过程非常短暂。 -
并发标记(concurrent-marking)
:与用户线程并发标记。这个阶段在初始标记的基础上继续向下追溯标记。在并发标记阶段,用户线程和标记线程并发执行,所以用户不会感受到停顿。**遍历第一个阶段(Init Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象在这个阶段,发生变化的对象标记为
Dity**
-
并发预清理(concurrent-precleaning)
:与用户线程并发进行。在并发标记阶段一些对象的引用已经发生了变化,precleaning
会发现这些引用关系的改变,并将存活的对象标记。举个例子:如果线程A有一个指向对象X的引用,并将该引用传递给了线程B,CMS需要记录下线程B持有了对象X,即使线程A已经不存在了。precleaning
是为了减少下一阶段“重新标记”的工作量,因为remark
阶段会STW
。将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象
-
重新标记(remark)
:remark
阶段会STW
。如果应用正在并发运行且在不断地改变对象引用,CMS
则不能准确地确定某个对象是否存活。所以CMS
会在remark
阶段STW
,从而获取所有引用关系的改变。 -
并发清理(concurrent-sweeping)
:清理垃圾对象,这个阶段GC线程和用户线程并发执行。 -
并发重置(concurrent-reset)
:重置CMS收集器的数据结构,做好下一次执行GC任务的准备工作。
可以看出,一个存在2次的STW