为什么 Java 中 CMS 垃圾收集器在发生 Concurrent Mode Failure 时的 Full GC 是单线程的
·
这个问题涉及到 CMS(Concurrent Mark-Sweep)垃圾收集器的设计原理和失败处理机制。要理解为什么在 Concurrent Mode Failure 时触发的 Full GC 是单线程的,得从 CMS 的工作流程和失败场景说起。
1. CMS 的正常工作流程
CMS 是一种以 低停顿时间 为目标的并发收集器,主要分为 4 个阶段:
- 初始标记(Initial Mark):标记 GC Roots 直接关联的对象(需 STW,但时间短)。
- 并发标记(Concurrent Mark):并发遍历对象图,标记存活对象(不 STW)。
- 重新标记(Remark):修正并发标记期间变动的对象(需 STW)。
- 并发清除(Concurrent Sweep):清理垃圾对象(不 STW)。
关键点:CMS 的并发设计是为了减少 STW 时间,但它的并发清理阶段不压缩内存(会产生碎片),且无法处理并发期间新产生的垃圾。
2. 什么是 Concurrent Mode Failure?
当 CMS 在执行 并发标记 或 并发清除 时,如果应用程序同时快速分配新对象,导致老年代空间被填满(晋升失败或分配失败),此时 CMS 无法继续并发工作,就会触发 Concurrent Mode Failure。
常见原因:
- 老年代剩余空间不足(比如对象晋升速度 > CMS 并发清理速度)。
- 内存碎片过多,无法分配大对象。
- 手动调优不当(如
-XX:CMSInitiatingOccupancyFraction设置过高)。
3. 为什么 Full GC 是单线程的?
当发生 Concurrent Mode Failure 时,JVM 会退回到 Serial Old 收集器(单线程的标记-整理算法)进行 Full GC。这是由 设计选择 和 安全保证 决定的:
(1) 设计上的保守策略
- CMS 的并发阶段已经占用了部分 CPU 资源,如果在失败时再启用多线程 GC(如 Parallel Old),可能进一步加剧资源竞争,导致系统卡死。
- 单线程 Serial Old 更稳定:虽然慢,但能确保在极端情况下安全地完成垃圾回收(避免多线程并发带来的复杂问题,如竞争、死锁等)。
(2) 内存碎片问题
- CMS 不整理内存,碎片化严重时,多线程压缩(如 Parallel Old)需要复杂的同步机制来移动对象。
- Serial Old 的标记-整理算法 是单线程顺序压缩,实现简单且可靠。
(3) 快速失败恢复
- 在 Concurrent Mode Failure 发生时,JVM 需要 立即 回收内存以保证程序继续运行。单线程 Serial Old 虽然慢,但它的算法是现成的、无需额外资源协调的“保底方案”。
- 多线程收集器(如 G1)的初始化或切换可能需要更多时间和资源,不适合紧急场景。
4. 如何避免 Concurrent Mode Failure?
既然 Full GC 退回到单线程代价高昂,通常通过调优规避:
- 预留更多空间:降低
-XX:CMSInitiatingOccupancyFraction(默认 92%),提前触发 CMS。 - 增加并行线程数:
-XX:ParallelGCThreads加快 CMS 标记/清理速度。 - 禁用浮动垃圾:
-XX:+CMSScavengeBeforeRemark在重新标记前强制 Young GC。 - 升级收集器:换用 G1 或 ZGC,它们对并发失败的处理更高效。
5. 对比其他收集器的行为
- G1:在类似失败时会触发 Full GC(多线程),因为 G1 本身是多线程设计的,没有历史包袱。
- ZGC/Shenandoah:几乎完全并发,极少触发 Full GC。
总结
CMS 在 Concurrent Mode Failure 时退回到单线程 Serial Old,本质是 工程上的权衡:
- 优先保证安全性:单线程能稳定完成回收,避免多线程在极端情况下的不可控问题。
- 历史局限性:CMS 是早期并发收集器,设计时未考虑多线程失败恢复(后来 G1/ZGC 改进了这一点)。
这种设计也提醒我们:低延迟的并发 GC 是有代价的,调优时要预留足够缓冲空间。
更多推荐


所有评论(0)