【JVM原理详解】27-CMS收集器原理与调优

发布时间:2026/7/31 23:58:06
【JVM原理详解】27-CMS收集器原理与调优 27-CMS 收集器原理与调优前面讲的收集器Serial、ParNew、Parallel Scavenge在回收老年代时都必须 STW堆越大停顿越长。对于交互式 Web 服务几百毫秒的停顿就可能导致请求超时。CMSConcurrent Mark Sweep是 HotSpot 第一款让老年代回收与用户线程并发进行的收集器把停顿从秒级降到百毫秒级曾长期是低延迟场景的首选。本篇深入剖析 CMS 的四阶段流程、三色标记算法、Concurrent Mode Failure 与内存碎片问题并给出生产级调优参数清单。CMS 的四个阶段CMS 的老年代回收分为四个阶段其中两个是并发的应用线程不停阶段1初始标记 (Initial Mark) ── STW极短 阶段2并发标记 (Concurrent Mark) ── 并发最耗时 阶段3重新标记 (Remark) ── STW较短 阶段4并发清除 (Concurrent Sweep)── 并发清理垃圾时间线应用线程: ████░░███████████████████░░██████████████████░░█████████ ↑ IM ↑ RM ↑ CS STW STW 并发 GC 线程: ──── 并发标记 ──── ──── 并发清除 ────初始标记Initial MarkSTW但耗时极短。它只标记GC Roots 能直接关联的对象即 GC Roots 的一跳邻居不遍历整个对象图。GC Roots → A → B → C → ... ↑ 初始标记只走到这一层由于范围小停顿通常几毫秒到几十毫秒。这一步依赖 ParNew 做新生代回收Minor GC来减少跨代引用扫描。并发标记Concurrent Mark并发耗时最长但不停应用。从初始标记的根开始遍历整个对象图标记所有可达对象。期间应用线程还在跑可能产生新的引用变化。这里的核心挑战是标记过程中对象引用关系在变如何保证标记正确这引出了三色标记算法。重新标记RemarkSTW暂停应用线程修正并发标记期间因引用变化导致的标记偏差。这是 CMS 中第二个 STW 点停顿通常比初始标记长但远短于并发标记。重新标记会处理并发标记期间新产生的引用——主要是通过SATBSnapshot-At-The-Beginning思路和增量更新机制确保漏标对象被补标。并发清除Concurrent Sweep并发清理未被标记的对象回收空间。这一阶段不停应用线程所以即使耗时较长也不影响延迟。缺点是清理后会产生内存碎片详见后文。三色标记算法并发标记的理论基础是三色标记Tri-color Marking把对象分为三种状态白色尚未被标记候选垃圾。灰色已被标记但其引用的对象还没全部标记待处理。黑色已被标记且其引用的对象也已全部标记存活不会被回收。初始所有对象为白色GC Roots 为灰色 过程从灰色对象出发将其引用对象标灰自身标黑 结束剩余白色对象即为垃圾[黑]A ──→ [灰]B ──→ [白]C ──→ [白]D │ └──→ [黑]E并发标记的漏标问题并发标记时应用线程可能修改引用导致两种问题问题1黑色对象指向白色对象新增引用标记前[黑]A [白]CC 即将被回收 标记后[黑]A ──→ [白]C A 新引用了 CA 已标记为黑色不会再扫描所以 C 不会被标记——但 C 实际已被引用不该回收。这就是漏标会导致存活对象被误回收是致命错误。问题2灰色对象断开到白色对象的引用标记前[灰]B ──→ [白]C C 等待 B 扫描 标记后[灰]B ──✗ B 断开了对 C 的引用但 C 被其他黑对象引用如果 B 是唯一会扫描到 C 的灰色对象断开后 C 永远不会被标灰——同样漏标。CMS 的解决方案增量更新CMS 用增量更新Incremental Update解决问题1当黑色对象新指向白色对象时记录这次写操作。重新标记阶段把这些黑色→白色的引用重新扫描一遍补标为灰色。// 伪代码CMS 的写屏障voidfieldWrite(ObjectblackObj,Fieldf,ObjectwhiteObj){if(isBlack(blackObj)isWhite(whiteObj)){cardTable.mark(blackObj);// 记录到 Card Table}// 实际写入blackObj.fwhiteObj;}重新标记时遍历 Card Table重新扫描这些黑色对象。这就是重新标记存在的根本原因——它要补救并发期间的漏标。注意G1 用的是SATB在并发开始时拍快照思路不同但目标一致下一篇会详述。Concurrent Mode FailureCMS 并发回收的代价是回收速度跟不上分配速度时老年代空间不够用。这时触发Concurrent Mode Failure。触发条件CMS 在并发阶段标记或清除期间应用线程还在分配对象进入老年代。如果老年代空间耗尽而 CMS 还没回收完就会触发 Full GC退化为 Serial Old单线程、STW、Mark-Compact。整个堆停顿可能几秒到十几秒。时间线 应用分配: ──→ 老年代快满 ──→ 继续分配 ──→ 老年代耗尽 ──→ Concurrent Mode Failure CMS 回收: 并发标记中 ──────────────→ 还没回收完 ──→ 降级 Serial Old为什么降级为 Serial Old因为此时老年代空间已耗尽无法继续并发回收并发回收需要额外空间做标记。唯一能做的就是 STW、全堆整理——而 CMS 自身没有 STW Full GC 实现只能借用 Serial Old。这是 CMS 最大的痛点一次 Concurrent Mode Failure 可能让停顿从 50ms 飙到 5 秒对延迟敏感的系统是灾难。预防策略调低触发阈值让 CMS 提早开始回收见CMSInitiatingOccupancyFraction。扩大老年代给回收留出更多空间余量。降低分配速率优化业务代码减少大对象分配。内存碎片问题CMS 用标记-清除Mark-Sweep算法不整理内存。长期运行后老年代会出现大量碎片[占用][空][占用][空空][占用][空][占用][空空空] ↑ 碎片总和够但不连续当需要分配一个 5MB 大对象碎片总和有 10MB 但最大连续块只有 2MB 时分配失败——触发 Full GC 整理碎片。Full GC with Compact碎片严重时CMS 会触发一次Full GC CompactSerial Old 单线程整理STW。整理后碎片消失但停顿很长。[占用][空][占用][空空][占用] → [占用占用占用][空空空空空空] 整理前碎片 整理后连续这就是 CMS 虽然标榜低延迟但偶尔的长停顿常被诟病的原因日常 GC 很快但碎片触发 Full GC 时可能数秒。关键调优参数-XX:CMSInitiatingOccupancyFraction设置老年代占用率阈值达到后触发 CMS 回收-XX:CMSInitiatingOccupancyFraction70默认值是92%JDK 8太高了——意味着老年代用到 92% 才开始回收留给并发回收的空间很紧容易 Concurrent Mode Failure。生产建议设70-80给并发回收留足余量。配合-XX:UseCMSInitiatingOccupancyFraction使用JDK 8 需显式开启手动模式-XX:UseConcMarkSweepGC\-XX:UseCMSInitiatingOccupancyFraction\-XX:CMSInitiatingOccupancyFraction75-XX:UseCMSCompactAtFullCollection开启后每次 Full GC 后做内存整理默认开启。关闭则只清除不整理停顿短但碎片累积。-XX:UseCMSCompactAtFullCollection# 开启整理默认-XX:CMSFullGCsBeforeCompaction0# 多少次 Full GC 后整理0每次都整理由于 CMS Full GC 已经是 STW整理多花的几十毫秒通常可接受。一般保持默认。-XX:CMSScavengeBeforeRemark重新标记前先做一次 Minor GC。好处新生代里的垃圾先清掉减少重新标记要扫描的跨代引用缩短重新标记停顿。-XX:CMSScavengeBeforeRemark强烈建议开启尤其当新生代较大时。代价是多了次 Minor GC但通常物有所值。-XX:ParallelCMSThreadsCMS 并发线程数默认(ParallelGCThreads 3) / 4。太多会挤占应用 CPU太少回收跟不上。一般保持默认CPU 紧张时可下调。调优参数清单下面是一份生产级 CMS 参数模板JDK 8按需调整java-Xms4g-Xmx4g-Xmn1g\-XX:UseConcMarkSweepGC-XX:UseParNewGC\-XX:CMSInitiatingOccupancyFraction70\-XX:UseCMSInitiatingOccupancyFraction\-XX:CMSScavengeBeforeRemark\-XX:UseCMSCompactAtFullCollection\-XX:CMSFullGCsBeforeCompaction0\-XX:ParallelGCThreads8\-XX:ConcGCThreads2\-XX:PrintGCDetails-XX:PrintGCDateStamps\-Xloggc:/var/log/gc.log\-cpMyApp com.example.Main要点堆固定-Xms -Xmx避免堆扩展触发额外 GC。新生代 1GB约占堆 25%给老年代留足空间。阈值 70%老年代到 70% 就回收余量充足。重新标记前 Minor GC减少重新标记停顿。GC 日志必开用于事后分析。代码示例制造 CMS Full GC/** * 演示 CMS 行为与 Concurrent Mode Failure * 适用 JDK 8 * * 运行 * java -Xms1g -Xmx1g -Xmn256m * -XX:UseConcMarkSweepGC -XX:UseParNewGC * -XX:CMSInitiatingOccupancyFraction70 * -XX:UseCMSInitiatingOccupancyFraction * -XX:PrintGCDetails -XX:PrintGCDateStamps * -Xloggc:gc.log -cp MyApp CmsGcDemo */publicclassCmsGcDemo{staticfinalint_1MB1024*1024;staticfinaljava.util.Listbyte[]CACHEnewjava.util.ArrayList();publicstaticvoidmain(String[]args)throwsException{// 持续往老年代填充制造压力for(inti0;i1000;i){CACHE.add(newbyte[2*_1MB]);// 大对象直接进老年代if(i%50)CACHE.subList(0,CACHE.size()/2).clear();// 偶尔清理制造碎片Thread.sleep(10);}}}观察日志中的关键字段# 并发标记开始 [2026-07-17T10:00:01.234] [GC [CMS-concurrent-mark-start] # 并发标记结束 [2026-07-17T10:00:02.567] [CMS-concurrent-mark: 1.333/1.333 secs] # 初始标记STW [2026-07-17T10:00:01.000] [GC (CMS Initial Mark) [1 CMS-initial-mark: 600M(768M)] 800M(1024M), 0.012 secs] # 重新标记STW [2026-07-17T10:00:02.400] [GC (CMS Final Remark) [1 CMS-remark: 650M(768M)] 850M(1024M), 0.045 secs] # Concurrent Mode Failure灾难 [2026-07-17T10:00:03.000] [GC (CMS Mode failure): 768M(768M) 1024M(1024M) 5.678 secs]看到CMS Mode failure就说明 CMS 没顶住触发了 Serial Old Full GC——这时就该调阈值或扩老年代了。CMS 的衰落CMS 在 JDK 9 被标记deprecatedJDK 14 被彻底移除。原因内存碎片Mark-Sweep 固有问题Full GC 整理停顿不可控。Concurrent Mode Failure高分配速率下不可预测的降级。浮动垃圾并发期间产生的垃圾本轮回收不掉。代码维护负担CMS 实现复杂G1 已经能更好满足低延迟需求。新项目不应再用 CMS。JDK 11 直接用 G1JDK 15 考虑 ZGC/Shenandoah。但理解 CMS 对学习 G1、ZGC 的并发回收思路至关重要——它们都站在 CMS 的肩膀上。实践要点1. 阈值宁低勿高默认 92% 太激进。生产环境设 70-80宁可多回收几次也别等耗尽。2. 监控 Concurrent Mode FailureCMS 日志里出现concurrent mode failure就是红线报警。即使一周一次也要追查原因——通常是分配速率突增或老年代太小。3. 大对象是 CMS 杀手大对象如大数组、大字符串直接进老年代快速消耗空间。如果业务有大量大对象分配考虑限制单次分配大小。复用缓冲区避免频繁创建。4. 浮动垃圾不可避免并发标记期间产生的垃圾本轮回收不掉只能等下次。这是 CMS 并发设计的固有代价无法消除只能靠提高回收频率缓解。5. 迁移到 G1 的时机JDK 升级到 11直接切 G1。堆超过 4GBG1 分区回收比 CMS 更可控。Concurrent Mode Failure 频发G1 的 Mixed GC 能更好处理碎片。小结CMS是第一款让老年代回收与用户线程并发的收集器四个阶段中初始标记和重新标记STW并发标记和并发清除与应用并发。三色标记黑灰白是并发标记的理论基础CMS 用增量更新解决黑色对象新引用白色对象的漏标问题。Concurrent Mode Failure是 CMS 最大的痛点老年代耗尽时降级为 Serial Old停顿可能数秒靠调低触发阈值预防。内存碎片是 Mark-Sweep 的固有缺陷靠UseCMSCompactAtFullCollection在 Full GC 后整理缓解。关键调优参数CMSInitiatingOccupancyFraction70、CMSScavengeBeforeRemark、固定堆大小JDK 14 后 CMS 已移除新项目应迁移到 G1 或 ZGC。下一篇我们进入 G1——它用分区化设计解决了 CMS 的碎片和 Full GC 不可控问题是 JDK 9 的默认收集器也是现代 JVM 调优的核心。更多内容JVM调优实战