
本篇是「JVM 与性能调优系列」第 6 篇。G1 之前CMS 是低延迟的代名词。它敢在程序运行的同时回收老年代。但它也留下了「碎片化的诅咒」最终被官方废弃。看懂 CMS才懂 G1 为什么是答案。一、CMS 要解决什么Parallel Old 回收老年代时是全程 STW堆一大停顿动辄几百毫秒——对 Web 服务是灾难。CMSConcurrent Mark Sweep的目标很明确降低老年代回收的 STW让大部分回收工作和业务线程并发进行。核心思想把「能并发的尽量并发」只保留极短的 STW 段。代价是算法更复杂、有碎片副作用。二、CMS 的四个阶段CMS 的老年代回收分四步其中两步极短 STW两步完全并发初始标记STW极短只标记 GC Roots直接关联的对象数量少快并发标记并发从初始标记的对象出发沿引用链遍历整个老年代。这段时间业务线程照常跑重新标记STW较短并发标记期间业务线程可能改了引用关系导致标记有偏差。这一步把变动修正过来用「增量更新」并发清除并发清除死亡对象用标记-清除算法不挪动存活对象注意第 2、4 步是并发的所以业务几乎不感知——这正是 CMS 低延迟的来源。但「重新标记」阶段仍要 STW它扫的是并发期间变动的引用通常比初始标记略长。三、三色标记与重新标记并发标记用三色标记追踪存活白尚未访问可能是垃圾灰已访问自身但引用还没扫完黑自身和引用都扫完确定存活问题来了并发标记时业务线程可能「把一个白对象挂到黑对象下同时断开灰对象到它的引用」——白对象会被误判为垃圾漏标。CMS 用**增量更新Incremental Update**补救只要黑对象新插入了白对象的引用就把它记下来重新标记时重新扫。G1 换了个思路用 SATB下一篇讲。两种都是「并发标记如何不漏标」的解法。四、CMS 的两大原罪CMS 证明了并发回收的价值却栽在两个硬伤上原罪 1碎片。CMS 用标记-清除不清空后不整理老年代会留下越来越多空洞。某天要分配一个大对象总空闲够却找不到连续空间 → 晋升失败 →退化成 Serial Old 做一次 Full GC巨慢。原罪 2并发失败。并发清除期间业务还在不停 new 对象老年代被填充。若填充速度超过清除速度、预留空间默认 92% 触发被击穿 →Concurrent Mode Failure→ 同样退化成 Serial Old。五、关键参数参数作用-XX:UseConcMarkSweepGC启用 CMS-XX:CMSInitiatingOccupancyFraction老年代占用到多少触发 CMS默认 92%-XX:UseCMSCompactAtFullCollectionFull GC 时做碎片整理-XX:CMSFullGCsBeforeCompaction每隔几次 Full GC 整理一次-XX:CMSParallelRemarkEnabled重新标记阶段并行缩短 STW调优要点如果频繁 Concurrent Mode Failure把CMSInitiatingOccupancyFraction调低比如 70%让 CMS 更早启动但太低会 GC 太频繁。六、为什么被废弃JDK 9 标记为废弃JDK 14彻底移除维护成本高、碎片化无解、无法与新特性如 ZGC 的染色指针共存官方明确推荐迁移到G1JDK 9 起默认今天新项目基本不该用 CMS 了但理解它仍重要——很多老系统还在跑而且 G1 的设计正是对 CMS 缺陷的回应。七、承上启下CMS 用「并发标记 标记清除」把老年代停顿降下来却败给碎片。G1 的解决方案是把堆切成小 Region每次只回收价值最高的 Region并在回收时做整理——既并发又无碎片。八、实战一次 Concurrent Mode Failure 复盘某电商服务老年代 4GCMSInitiatingOccupancyFraction92。大促时突发流量对象以 300MB/s 的速度晋升老年代。CMS 在 92%约 3.7G才启动并发标记但标记清除速度跟不上业务填充速度老年代很快冲破上限 → 触发Concurrent Mode Failure→ 退化 Serial Old 全堆整理 →一次 2.3 秒的全局停顿接口全部超时。根因不是 CMS 不行是触发阈值太高。把CMSInitiatingOccupancyFraction调到 70%让 CMS 提前启动并发阶段有充足时间跟上业务速度问题消失。九、CMS 与日志的对应开启-XX:PrintGCDetails后CMS 的周期在日志里长这样CMS-initial-mark → CMS-concurrent-mark → CMS-concurrent-preclean → CMS-remark (STW) → CMS-concurrent-sweep → CMS-concurrent-reset看到concurrent mode failure字样就对应上面的原罪 2优先调低触发阈值。十、CMS 废弃后的迁移路径如果你的老系统还在用 CMS-XX:UseConcMarkSweepGCJDK 14 会直接启动失败。迁移建议首选 G1JDK 9 默认几乎无需改业务代码停顿通常比 CMS 更稳、无碎片。多数 CMS 应用平迁 G1 即可低延迟需求迁 ZGC若对停顿极敏感且能升 JDK 17/21ZGC 是终极答案保留 Parallel 的场景纯吞吐批处理且不想动 JDKParallel 也完全够用迁移不是换个参数那么简单要先在测试环境压测对比停顿与吞吐确认新收集器在你的真实负载下达标再上生产。别信「换个收集器自动变快」的神话。十一、CMS 并发模式失败深度剖析Concurrent Mode Failure是 CMS 最典型的崩溃值得深挖它的触发链CMS 在老年代占用到CMSInitiatingOccupancyFraction默认 92%时才启动并发周期并发标记 并发清除期间业务线程仍在疯狂分配老年代持续被填充如果填充速度 清除速度老年代在周期结束前就见了底CMS 无力回天 → 冻结所有线程 → 退化成Serial Old做一次全堆、单线程、标记-整理的 Full GC这次退化 Full GC 可能耗时数秒堆越大越久期间所有请求超时。根因不是 CMS 慢是触发太晚 分配太快。把CMSInitiatingOccupancyFraction调到 70%~80%让并发周期早启动、留足缓冲就能避免。总结CMS 是低延迟的先驱初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除把大部分工作并发化。但它用标记-清除留下碎片并发期间又可能并发失败两者都逼它退化成 Serial Old。它的退役恰好引出了下一篇的主角 G1。