CMS初始标记必须STW,因需冻结线程确保GC Roots不被修改,避免漏标;耗时通常<10ms,异常延迟多因safepoint进入慢;并发标记用增量更新+写屏障捕获引用变化;预清理阶段并发清除浮动垃圾以缩短Remark;并发失败将触发Full GC,主因是老年代填充快于清理;CMS已在JDK14移除。

CMS 的初始标记为什么必须 STW?
因为初始标记只扫描 GC Roots 直接关联的对象,看似轻量,但 JVM 必须冻结所有应用线程,确保 GC Roots(如栈帧中的局部变量、静态字段、JNI 引用)不会在扫描过程中被修改。一旦允许并发,mutator 可能刚把一个对象从栈弹出,而标记线程还没扫到它,就会漏标。
- 这个阶段耗时极短,通常不到 10ms,但它是 STW 的刚性要求,无法绕过
- 如果发现
Initial Mark阶段耗时异常(比如 >50ms),大概率是线程数过多导致 safepoint 进入延迟高,不是 CMS 本身问题 - 不要试图用
-XX:+CMSParallelInitialMarkEnabled(已废弃),JDK 7u4 之后该参数无效
并发标记阶段如何处理对象引用变动?
CMS 用“增量更新”(incremental update)机制捕获并发期间的新引用:当 mutator 修改一个老年代对象的字段,指向另一个老年代对象时,JVM 会通过写屏障(write barrier)把被修改的字段所在卡页(card)标记为“脏”,后续重新标记阶段再扫描这些脏卡。
- 写屏障开销小,但会略微拖慢每次对象字段赋值——这是 CMS 吞吐略低于 Parallel GC 的主因之一
- 如果应用大量执行
obj.field = anotherObj(尤其在老年代对象间),脏卡堆积多,会导致重新标记时间飙升 - 可通过
-XX:+PrintGCDetails观察日志中[Rescan (parallel)和[Remark的耗时对比,若后者显著更长,说明并发期间变动太剧烈
预清理和可中断预清理到底在清理什么?
这两个子阶段不标记对象,而是为最终的 Remark 做减法:扫描并清除那些在并发标记期间变成“不可达”、但尚未被回收的浮动垃圾(floating garbage),同时尽量把一部分 Remark 工作提前做掉,缩短 STW 时间。
-
Abortable Preclean会尝试运行一段时间(默认最多 5 秒),如果发现老年代空闲空间下降太快(比如initiating occupancy fraction快要触发),就主动中止,避免耽误 CMS 周期 - 如果日志频繁出现
Abortable Preclean interrupted,说明 CMS 触发太晚或老年代分配压力过大,应调低-XX:CMSInitiatingOccupancyFraction - 这两个阶段完全并发,不 STW,但会占用 CPU 资源;在 CPU 紧张的容器环境里,可能加剧响应毛刺
CMS 的并发失败(concurrent mode failure)怎么救?
当并发周期还没完成,老年代就填满了,CMS 被迫退化为单线程 Full GC(使用 Serial Old 收集器),整个过程 STW,且耗时远超正常 CMS 周期。
- 典型错误日志:
Concurrent Mode Failure: … triggering Full GC—— 这不是警告,是已发生的灾难性回退 - 根本原因通常是老年代晋升速率 > CMS 并发清理速率,常见于大对象直接进入老年代、或年轻代 GC 频繁导致大量对象晋升
- 临时缓解可用
-XX:CMSInitiatingOccupancyFraction=60提前启动 CMS,但治标不治本;长期需压测定位对象生命周期异常点 - JDK 8u40 后 CMS 已被标记为 deprecated,JDK 14 彻底移除,生产环境继续用 CMS 就得自己担着没补丁的风险
CMS 的四个阶段不是线性流水线,而是带条件跳转和动态中止的协作机制;真正难调的从来不是参数本身,而是如何让应用行为适配这套“边干活边擦黑板”的节奏。








