因为CMS是java垃圾回收器历史上一重大的里程碑,涉及到的新概念有很多,所以选择单独一篇出来讲。
一分钟看懂CMS
CMS是能够与应用程序并发工作的老年代收集器,它主要使用 Mark-Sweep
算法清理垃圾。由于CMS的并发工作特点,它的垃圾收集时间短,响应速度快,延迟低,但是它也不是十全十美的,它的吞吐量因为CPU资源竞争会降低一些,算法选择上容易产生内存碎片化,工作方式上容易产生浮动垃圾,易出现promotion failed和并发故障(Concurrent Mode Failure)。
工作流程分析
CMS的清理严格区分,共分为7个阶段:
- 初始标记(STW)
- 并发标记
- 并发预清理
- 并发可终止预清理
- 最终标记(STW)
- 并发清理
- 并行复位
使用 📌
标记的阶段,是必然存在的阶段,其余的阶段可以通过配置参数取消
1. 📌(STW)初始标记
该阶段会标记所有老年代里的GC Roots、被年轻代引用的对象(重要),如下图所示:
2. 📌并发标记
这个阶段会与工作线程一起并发执行,工作内容有两个:
- 标记所有存活对象。 通过遍历“阶段1标记的起始对象”尽可能标记所有存活对象(因为标记时,工作线程也在执行,必然会有漏网之鱼,所以只能说尽可能)。
- 记录引用发生改变的对象。在对象被标记后,如若发生了改变,这个对象所在的一小片内存区域(这个区域也称为
Card
)也会被标记为Dirty
。
在阶段一里, Current Obj
本来是和下面的圆圈有连接的,但是被标记后就因为工作线程断开了。那么这个对象所在的区域就会被标记为 Dirty
:Dirty Card
会在第三个阶段被处理。
3. 并发预清理
这个阶段同样是与工作线程共同执行的,该阶段的主要工作是处理 上个阶段产生的 Dirty Card
——把 Dirty Card
内发生改变的对象的相关联的对象标记起来,如下图所示:
一个 Dirty Card
区域处理完毕后,就会把这个 Card
的 Dirty
标志清除掉。
除此之外,还会执行一些必要的整理工作和为 Final Remark
阶段的准备工作。
4. 并发可中断预清理
这个阶段也是与工作线程共同执行的,该阶段主要循环之前的工作:
- 标记被新生代引用的老年代对象
- 扫描、处理
Dirty Card
并等待进入 Final Remark
的“时机”,这个“时机”受三个JVM参数影响:
CMSMaxAbortablePrecleanLoops
,默认是0,表示没有循环次数的限制。达到了循环次数会推出循环CMSMaxAbortablePrecleanTime
,默认是5S。如果执行这个逻辑的时间达到了,会退出循环CMSScheduleRemarkEdenPenetration
,默认50%。如果新生代Eden区的使用率达到了,会退出循环
该阶段主要想尽可能地标记所有老年代存活的对象,从而降低 Final Remark
的STW时间,降低GC的延迟。
为什么会有这个阶段,没有行不行?
我们可以这么考虑:“并发预清理阶段”只是处理了老年代的 Dirty Card
,此时并行工作的应用程序可能已经在年轻代引用了许多老年代对象了,如果没有“并发可中断预清理”这个步骤,那么 Final Remark
还需要完整的扫描整个年轻代,STW的时间就会变长。
所以这就有了上面三个影响进入 Final Remark
的“时机”的参数:
CMSMaxAbortablePrecleanLoops
如果太小了,可能还没执行一两次,并发可中断预清理就结束了。假设此时年轻代激增(遇到高峰期),到了Final Remark
阶段,CMS不仅需要扫描年轻代,还需要扫描老年代,无疑增长了STW的时间;一般该参数都是设置成0,即不断循环由另外两个参数决定退出,因为循环一次到底收集了多少不好判断。CMSMaxAbortablePrecleanTime
如果太小了,结果和上面的情况一样;如果太大了,该阶段处理时间就又太长(除非年轻代达到了CMSScheduleRemarkEdenPenetration
,否则就要等着)CMSScheduleRemarkEdenPenetration
如果太小了,容易触发Final Remark
,因为触发还需要时间,这个间隔期间,新生代大量激增,那么Final Remark
的STW又会变长;如果太大了,流量是低谷期的情况,该阶段会等待很久,又取决于CMSMaxAbortablePrecleanTime
。 | 参数名 | 方案1 | 方案2 | 方案3 | 方案4 | | —- | —- | —- | —- | —- | |CMSMaxAbortablePrecleanLoops
| 0 | 0 | 0 | 0 | |CMSMaxAbortablePrecleanTime
| 短 | 短 | 长 | 长 | |CMSScheduleRemarkEdenPenetration
| 小 | 小 | 大 | 大 | | 高峰期/低谷期 | 高峰期 | 低谷期 | 高峰期 | 低谷期 | | 最终Final Remark的STW | 长 | 短 | 相对短 | 长 |
该表仅供参考,仍需按照实际业务进行判断。
总而言之,这个阶段就是为了尽可能减少年轻代的大小,标记所有的存活的对象,减少 Final Remark
的STW时间。
5. 📌(STW)最终标记
这个阶段是需要STW的。因为前面的阶段都是与工作线程并发执行的,会有一些对象不会及时被标记,所以该阶段就是为了标记老年代所有存活的对象。通常CMS会尽可能在年轻代为空的时候执行该阶段,因为可以降低多个停顿阶段相继发生的可能性。
这个阶段后,老年代里的所有存活对象都被标记了,剩下的就是回收工作了
6. 📌并发清理
由于前面所有存活的对象都被标记了,该清理的对象也不会被使用到,所以这个阶段也是并行的。该阶段清理了那些无用的对象,并腾出内存空间。
7. 并发重置
重置CMS算法的内部数据结构,为下一次回收最好准备。
该阶段不是那么的重要,一般不会提到。
缺陷
并发模式失败(Concurrent Mode Failure)
正常情况下,CMS大部分时间都和应用程序一起工作,所以工作线程顶多只会被YGC短暂暂停一会,而当老年代不足以完成年轻代对象的晋升时,即CMS不能及时回收垃圾,腾出空间进行分配时,则所有应用程序停止。这种情况,就称为“并发模式故障”,此时就需要调整CMS收集器的参数。
As described previously, in normal operation, the CMS collector does most of its tracing and sweeping work with the application threads still running, so only brief pauses are seen by the application threads. However, if the CMS collector is unable to finish reclaiming the unreachable objects before the old generation fills up, or if an allocation cannot be satisfied with the available free space blocks in the old generation, then the application is paused and the collection is completed with all the application threads stopped. The inability to complete a collection concurrently is referred to as concurrent mode failure and indicates the need to adjust the CMS collector parameters. ————《Concurrent Mark Sweep (CMS) Collector》
并发模式中断(concurrent mode interruption)
如果并行回收过程中,被显示的GC( System.gc()
)或被内存诊断工具为了收集信息而打断时,CMS就会报出 并发模式中断的信息。
If a concurrent collection is interrupted by an explicit garbage collection (System.gc()) or for a garbage collection needed to provide information for diagnostic tools, then a concurrent mode interruption is reported. ————《Concurrent Mark Sweep (CMS) Collector》
浮动垃圾
由于并发清理阶段是和应用程序并行工作的,所以在清理过程中会诞生一些新的垃圾,这些无法在此次GC中回收掉的垃圾,需要留到下一次GC的垃圾称为浮动垃圾。
总结
总而言之,CMS垃圾收集器通过将大量工作分流到不需要停止应用程序的并发线程上,在减少STW时间方面做得很好。但是,它也有一些缺点,其中最明显的是老年代碎片化,并且在某些时候下,尤其是在大堆上,STW持续时间缺乏可预测性(就是停上几个小时都无法预测)。