Java虚拟机:CMS垃圾回收器
在 Java 虚拟机JVM的众多垃圾回收器中CMSConcurrent Mark Sweep是一个经典且具有里程碑意义的设计。它主要服务于对响应时间敏感的应用希望在垃圾回收过程中尽可能减少应用程序的停顿Stop-The-World, STW时间。本文将从 CMS 的设计目标、核心工作阶段、关键参数、日志分析以及优缺点等多个维度带你彻底吃透 CMS 回收器。1. CMS 是什么为什么需要它全称Concurrent Mark Sweep并发标记清除核心目标获取最短的回收停顿时间。适用场景互联网网站、B/S 系统服务端等对响应速度要求高的应用。与 ParallelGC并行回收器不同ParallelGC 追求高吞吐量在 GC 时会暂停所有应用线程而 CMS 追求低延迟它的大部分工作阶段都可以和用户线程并发执行因此整体停顿时间更短。2. CMS 的六大工作阶段图文详解CMS 的回收过程非常精细总共分为6 个步骤其中只有初始标记和重新标记需要 STW其余阶段均可与应用程序并发执行。下图清晰展示了 CMS 的工作流程标注STW的为停顿阶段阶段名称类型主要工作1. 初始标记 (Initial Mark)STW标记根对象GC Roots直接引用的对象速度很快。2. 并发标记 (Concurrent Mark)并发从根对象开始遍历整个老年代标记所有存活对象。此阶段时间长但应用线程可继续运行。3. 预清理 (PreClean)并发在重新标记之前提前处理一些并发标记阶段变化的对象目的是缩短后续 STW 时间。4. 重新标记 (Remark)STW修正并发标记期间因用户程序运行而发生变化的那部分对象标记。这是 CMS 中最耗时的停顿阶段。5. 并发清理 (Concurrent Sweep)并发清除未被标记的垃圾对象回收内存空间。应用线程继续执行但同时会产生新的垃圾称为“浮动垃圾”。6. 并发重置 (Concurrent Reset)并发重置 CMS 内部数据结构为下一次回收做准备。注意并发 ≠ 并行。并行多条 GC 线程同时工作但应用线程完全暂停。并发GC 线程与应用线程交替执行应用不中断。3. 内存碎片问题与解决CMS 基于“标记-清除”算法而非“标记-整理”或“复制”算法。这意味着它在回收后不会移动存活对象导致老年代出现大量不连续的内存碎片。下图示意了回收前后老年代空间的变化碎片带来的问题即使总剩余内存足够也可能因为找不到连续空间而无法分配一个大对象从而提前触发 Full GC。解决方案参数-XX:UseCMSCompactAtFullCollection在 CMS Full GC 后进行一次内存压缩整理整理过程 STW。-XX:CMSFullGCsBeforeCompactionn设定在 n 次 CMS 回收后强制执行一次压缩整理。4. 关键参数详解与调优建议CMS 提供了丰富的参数帮助我们根据应用特点精细调优。参数含义调优建议-XX:UseConcMarkSweepGC启用 CMS 回收器基本开关开启后老年代使用 CMS新生代默认用 ParNew。-XX:ConcGCThreads或-XX:ParallelCMSThreads设置并发线程数默认 (ParallelGCThreads 3) / 4。CPU 核心数较少时不宜设置过大否则会抢占应用 CPU。-XX:CMSInitiatingOccupancyFraction老年代使用率达到多少时触发 CMS 回收默认 68%。内存增长快可降低如 60%增长慢可提高如 80%减少 GC 频率。-XX:UseCMSCompactAtFullCollection是否在 Full GC 后整理内存建议开启以缓解碎片问题但会增加停顿时间。-XX:CMSFullGCsBeforeCompaction多少次 CMS 后执行一次压缩默认 0即每次 Full GC 后都压缩。可适当调大如 5平衡碎片与停顿。⚠️ 注意事项如果 CMS 回收期间内存使用率继续增长导致可用空间不足CMS 会回收失败此时 JVM 会退化为串行 Full GC单线程STW停顿时间会极长。因此合理设置CMSInitiatingOccupancyFraction至关重要要保证在 CMS 完成前应用有足够内存可用。5. 实战日志解读我们使用以下参数运行一段产生 OOM 的代码-Xms10m -Xmx10m -XX:PrintGCDetails -XX:UseConcMarkSweepGC截取关键日志片段并逐行解析已配合你提供的日志[GC (Allocation Failure) [ParNew: 1428K-34K(3072K), 0.0016320 secs] [CMS (concurrent mode failure): 5709K-2754K(6848K), 0.0042625 secs] 5745K-2754K(9920K)]ParNew新生代 GC回收后 1428K → 34K。concurrent mode failureCMS 并发回收时老年代又满了导致 CMS 失败触发 Full GC。随后出现[Full GC (Allocation Failure) [CMS: ...]表明老年代串行回收启动。[GC (CMS Initial Mark) [CMS Initial Mark: 4816K(6848K)] 4816K(9920K), 0.0004828 secs]初始标记STW 0.48ms标记根对象速度很快。[CMS-concurrent-mark-start] java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(...)并发标记开始但此时应用因堆内存耗尽抛出 OOM。说明CMSInitiatingOccupancyFraction触发阈值过高导致还没完成回收内存就满了。[CMS-concurrent-preclean: 0.000/0.000 secs] [GC (CMS Final Remark) [YG occupancy: 81 K (3072 K)] [Rescan (parallel), 0.0002124 secs] ...]预清理和重新标记STW这里耗时极短说明存活对象不多。[CMS-concurrent-sweep-start] [CMS-concurrent-sweep: 0.000/0.000 secs] [CMS-concurrent-reset: 0.000/0.000 secs]并发清理和重置完成速度很快因为此时堆几乎已满清理量少。日志小结该案例中堆内存10MB太小且触发阈值默认68%过晚导致 CMS 还在并发标记时应用就触发了 OOM。解决方案增大堆内存或调低-XX:CMSInitiatingOccupancyFraction如 50%提前启动 CMS。6. CMS 的优缺点总结优点缺点✅低停顿大部分阶段并发GC 停顿短❌CPU 敏感并发阶段会占用应用 CPU 资源✅响应快适合交互式应用❌无法处理浮动垃圾需预留空间可能触发 concurrent mode failure✅ 可配合 ParNew 新生代并行回收❌内存碎片需额外压缩增加 STW❌ 实现复杂JDK9 后已被标记为废弃G1 替代