一、CMS垃圾回收器简介
CMS(Concurrent Mark-Sweep)是JVM中最早实现并发垃圾回收的收集器之一,主要目标是最小化应用停顿时间,适合对响应延迟敏感的服务端应用(如Web服务器、在线交易系统等)。
- 适用JVM参数:
-XX:+UseConcMarkSweepGC - 主要针对老年代进行回收(新生代默认用ParNew)
二、工作原理与生命周期
CMS采用“标记-清除”算法,核心思想是让大部分GC工作与应用线程并发执行,从而减少Stop-The-World(STW)停顿。
1. 回收流程
CMS老年代回收大致分为4个阶段:
-
初始标记(Initial Mark)
- STW短暂停顿
- 标记所有GC Roots直接可达对象
-
并发标记(Concurrent Mark)
- 与应用线程并发
- 从GC Roots出发,扫描对象图,标记所有可达对象
-
重新标记(Remark)
- STW,时间较短
- 处理并发标记期间新产生的引用变动(通过“增量更新”或“原始快照”)
-
并发清除(Concurrent Sweep)
- 与应用线程并发
- 回收所有未被标记的对象,释放空间
注意: 新生代通常由ParNew(多线程)回收,老年代由CMS回收。
三、算法细节与实现机制
1. 标记-清除算法
- 标记阶段:遍历对象图,标记所有可达对象。
- 清除阶段:回收未被标记的对象,空间可能产生碎片。
2. 并发与STW
- 只有初始标记和重新标记需要STW,时间短。
- 并发标记、并发清除与应用线程并发,减少应用停顿。
3. 写屏障与卡表
- 为了支持并发标记,JVM采用写屏障(Write Barrier)机制记录对象引用的变动。
- 典型实现有“增量更新(Incremental Update)”和“原始快照(Snapshot-at-the-Beginning)”。
4. 空间碎片问题
- CMS采用清除而非压缩,容易产生老年代碎片,可能导致“Promotion Failed”或“Full GC”。
四、优缺点分析
优点
- 低延迟:STW时间短,适合对响应时间敏感的应用。
- 高并发:大部分GC工作与应用线程并发进行。
缺点
- 空间碎片:采用标记-清除,老年代易碎片化,严重时触发Full GC。
- 并发失败:如果回收速度跟不上分配速度,可能“Concurrent Mode Failure”,导致Full GC,长时间STW。
- CPU资源消耗大:并发阶段与应用线程争抢CPU,整体吞吐量下降。
- 已被G1等新一代GC替代:JDK9以后已标记为“即将废弃”。
五、常用JVM参数与调优
1. 启用CMS
-XX:+UseConcMarkSweepGC
2. 配合ParNew新生代收集器
-XX:+UseParNewGC
3. 控制并发线程数
-XX:ParallelCMSThreads=4 # CMS并发线程数
-XX:ConcGCThreads=4 # 并发GC线程数(JDK8+)
4. 空间碎片与Full GC调优
-
自动空间压缩(默认关闭)
-XX:+UseCMSCompactAtFullCollection
Full GC时对老年代进行压缩,减少碎片,但会增加停顿。 -
触发压缩的Full GC阈值
-XX:CMSFullGCsBeforeCompaction=2
每2次Full GC后进行一次压缩。
5. 回收阈值与触发时机
-
老年代使用率阈值
-XX:CMSInitiatingOccupancyFraction=70
老年代使用率达到70%时触发CMS回收。 -
允许在CMS期间分配失败自动转Full GC
-XX:+CMSFullGCsBeforeCompaction
6. 其他常用参数
-XX:+CMSScavengeBeforeRemark:在Remark阶段前进行一次Minor GC,提升回收效率。-XX:+CMSClassUnloadingEnabled:允许卸载无用类元数据,减少Metaspace压力。
六、常见问题与工程实践
1. Concurrent Mode Failure
- 回收速度跟不上分配速度,老年代被填满,JVM被迫触发Full GC(STW时间长)。
- 解决方法:
- 提前触发CMS(降低CMSInitiatingOccupancyFraction)
- 增大老年代空间
- 优化应用对象分配和生命周期
2. Promotion Failed
- 新生代晋升到老年代失败,可能因碎片或空间不足。
- 解决方法:
- 增大老年代
- 调整分区比例
- 优化对象分代策略
3. 碎片导致频繁Full GC
- CMS不压缩空间,老年代碎片多时大对象分配失败。
- 解决方法:
- 启用Full GC时压缩
- 及时升级到G1等支持并发压缩的GC
4. CPU资源竞争
- CMS并发阶段与应用线程争抢CPU,业务高峰期可能影响吞吐。
- 解决方法:
- 合理设置并发线程数
- 评估业务高峰期GC影响
七、GC日志分析
开启GC日志:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
CMS日志关键字段:
CMS-initial-mark:初始标记CMS-concurrent-mark:并发标记CMS-remark:重新标记CMS-concurrent-sweep:并发清除Full GC:Full GC发生concurrent mode failure:并发回收失败
八、CMS与其他GC的对比
| GC收集器 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| CMS | 低延迟服务端 | 停顿短、并发 | 碎片、Full GC慢 |
| G1 | 大堆、低延迟 | 并发+压缩、可预测 | 配置复杂、吞吐略低 |
| Parallel | 吞吐优先 | 吞吐高 | 停顿长 |
| ZGC/Shenandoah | 超大堆、极低延迟 | 停顿极短、并发压缩 | JDK11+/新、成熟度有限 |
九、结论与建议
- CMS适合对响应时间敏感、堆空间中等的服务端应用。
- 生产环境要重点关注碎片、Full GC、并发失败等问题。
- JDK9以后建议优先采用G1或ZGC等新一代GC。
- 持续关注GC日志,结合业务负载动态调优。
十、参考资料
- Oracle官方CMS文档
- 《深入理解Java虚拟机》第三版(周志明)
- CMS GC调优实战
- G1与CMS对比官方文档
十一、写屏障(Write Barrier)与卡表(Card Table)
1. 为什么需要写屏障?
在CMS的并发标记阶段,应用线程和GC线程同时运行。如果应用线程在GC标记时修改了对象引用(即“脏写”),会导致GC漏标记,造成误回收。
2. 写屏障机制
CMS采用写屏障(Write Barrier)机制拦截所有对象引用的写操作。当应用线程修改引用时,将被修改的“卡片”(Card)标记为脏(dirty),以便在重新标记(Remark)阶段重新扫描。
- 增量更新(Incremental Update):标记所有在并发标记后被修改过的对象。
- 原始快照(Snapshot-at-the-Beginning):标记所有在并发标记前已存在的引用。
CMS默认采用增量更新。
3. 卡表(Card Table)
- 堆被划分为固定大小的“卡片”(通常512字节)。
- 每个卡片有一个状态位,记录是否被修改。
- 写屏障会把对应卡片的状态置为脏。
十二、并发标记的难点与优化
1. 并发标记的挑战
- 应用线程持续创建和修改对象,GC线程很难捕捉所有变化。
- 写屏障和卡表机制保证了可达性分析的正确性,但带来一定性能开销。
2. 重新标记(Remark)阶段
- 由于并发期间有新引用产生,CMS需要STW下重新遍历脏卡片,确保所有可达对象都被正确标记。
- 这一步虽然STW,但比全表扫描快很多。
3. 与新生代GC的协作
- 在Remark前通常会触发一次Minor GC,减少新生代对老年代的引用变动。
十三、碎片问题底层分析
1. 标记-清除的本质
- CMS只回收不可达对象,不移动存活对象。
- 多次GC后,老年代会出现大量不连续的小空洞(碎片)。
2. 碎片的实际危害
- 大对象分配时,虽然总空间足够,但没有足够大的连续内存,导致分配失败(Promotion Failed)。
- JVM被迫触发Full GC,并进行内存压缩,造成长时间STW。
3. Full GC与压缩
- Full GC会暂停所有线程,移动存活对象,合并碎片,但代价大。
- CMS可通过参数定期触发压缩,但会丧失低延迟优势。
十四、参数调优实战
1. 常用参数说明
| 参数 | 作用 | 推荐设置 |
|---|---|---|
| -XX:+UseConcMarkSweepGC | 启用CMS | 必须 |
| -XX:CMSInitiatingOccupancyFraction=70 | 老年代使用率达到70%时触发CMS | 50-80,根据业务负载调整 |
| -XX:+UseCMSCompactAtFullCollection | Full GC时压缩老年代 | 建议开启 |
| -XX:CMSFullGCsBeforeCompaction=2 | 每2次Full GC后压缩一次 | 1-5,视碎片情况调整 |
| -XX:+CMSClassUnloadingEnabled | 允许卸载无用类元数据 | 建议开启 |
| -XX:+CMSScavengeBeforeRemark | Remark前先做一次Minor GC | 建议开启 |
| -XX:ParallelCMSThreads | 并发GC线程数 | 一般为CPU数/2或CPU数 |
2. 实践建议
- 内存充裕时,适当增大老年代,减少GC频率。
- 延迟敏感时,降低CMSInitiatingOccupancyFraction,提前回收,避免Concurrent Mode Failure。
- 碎片严重时,调低CMSFullGCsBeforeCompaction,增加压缩频率。
十五、GC日志精细解读
示例日志片段:
2019-01-01T12:00:00.000+0800: 1.234: [GC (CMS Initial Mark) [1 CMS-initial-mark: 12345K(24576K)] 23456K(32768K), 0.0123456 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-01-01T12:00:00.500+0800: 1.734: [GC (CMS Final Remark) [YG occupancy: 4096 K (8192 K)] 16384K(24576K), 0.0234567 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2019-01-01T12:00:01.000+0800: 2.234: [GC (CMS Concurrent Sweep) ...]
2019-01-01T12:00:05.000+0800: 6.234: [Full GC (Allocation Failure) ...]
- Initial Mark:初始标记,STW
- Concurrent Mark:并发标记
- Final Remark:重新标记,STW
- Concurrent Sweep:并发清除
- Full GC:分配失败,STW极长
关注点:
- Remark和Initial Mark耗时,是否过长
- 是否频繁出现Full GC
- 是否有Concurrent Mode Failure
十六、与G1/ZGC的迁移建议
1. 为什么迁移?
- CMS已被标记为废弃(JDK14+),后续不会维护
- G1支持并发标记+并发压缩,几乎无碎片问题
- ZGC、Shenandoah支持超大堆、极低延迟
2. 迁移步骤
- 评估业务延迟和吞吐需求
- JVM参数切换
- G1:
-XX:+UseG1GC - ZGC:
-XX:+UseZGC(JDK11+)
- G1:
- 调优目标停顿时间
- G1:
-XX:MaxGCPauseMillis=200 - ZGC:
-XX:MaxPauseMillis=10
- G1:
- 观察GC日志,调整堆大小、线程数、分区数
3. 兼容性与坑
- G1参数与CMS有差异,需重新调优
- ZGC对JDK版本有要求,部分监控工具尚不完善
十七、工程实践总结
- CMS适合对延迟敏感、堆空间不太大的中大型服务。
- 生产环境务必监控GC日志,重点关注碎片、Full GC、并发失败。
- 新项目建议优先选择G1/ZGC,老项目逐步迁移,CMS仅用于特殊场景。
- GC调优是动态过程,需结合业务压力、内存分配、对象生命周期持续优化。
十八、参考与工具
- GCeasy.io:GC日志可视化分析
- JClarity Censum:专业GC日志分析
- Oracle官方GC调优指南
- 深入理解Java虚拟机(周志明)
十九、底层实现与源码机制
1. CMS 源码核心流程(以 OpenJDK 为例)
CMS 在 HotSpot 源码中主要通过 ConcurrentMarkSweepGeneration 类实现,核心流程如下:
- Initial Mark:通过遍历 GC Roots(线程栈、本地变量、静态变量等),标记直接可达对象。
- Concurrent Mark:通过多线程并发遍历对象图,递归标记所有可达对象。采用任务队列分配给多个 GC 线程。
- Preclean:在 Remark 前,对新生代晋升到老年代以及写屏障记录的变更做一次预处理,减少 Remark 工作量。
- Remark:STW,处理并发期间未被及时标记的对象,通常采用增量更新算法。
- Concurrent Sweep:并发清除未标记对象,回收空间。
- Reset:重置标记位,为下次 GC 做准备。
2. 写屏障源码简析
写屏障在 CMS 主要通过 CardTableRS(Card Table Remembered Set)实现。每次对象引用被写入时,都会在卡表中记录该内存区域为“脏”,以便后续 Remark 阶段重新扫描。
二十、典型故障场景与排查
1. Concurrent Mode Failure
现象:GC 日志频繁出现 [GC (CMS) ... (concurrent mode failure)],应用长时间停顿。
原因:
- 老年代分配速度远快于 CMS 回收速度。
- CMS 触发太晚或并发线程数不足。
- 业务高峰期对象晋升过快。
排查方法:
- 检查
CMSInitiatingOccupancyFraction是否过高。 - 检查 GC 日志中 CMS 开始与 Full GC 的时间间隔。
- 观察老年代分配速率与回收速率。
2. Promotion Failed
现象:新生代对象晋升到老年代失败,导致 Full GC。
原因:
- 老年代碎片严重,无法分配大对象。
- 老年代空间不足。
排查方法:
- 观察 GC 日志中
Promotion failed、allocation failure相关字段。 - 用
jmap -heap或监控工具查看老年代碎片比例、最大可用连续空间。
3. Remark 阶段耗时过长
现象:GC 日志中 CMS-remark 耗时显著增加。
原因:
- 写屏障记录的变更过多,脏卡片数量激增。
- 新生代与老年代引用关系复杂。
排查方法:
- 业务代码是否有大量对象间引用变动。
- GC 日志分析卡表扫描量。
二十一、与 JVM 内存模型的关系
- CMS 主要负责老年代(Tenured Generation),新生代由 ParNew 或其他收集器负责。
- 老年代空间配置过小或分代比例不合理,会加剧 CMS 的压力。
- 元空间(Metaspace/Class Metadata)可用
CMSClassUnloadingEnabled参数控制卸载,防止类元数据泄漏。
二十二、监控与自动化工具
1. GC 日志分析
- GCeasy.io:在线 GC 日志分析,支持 CMS、G1、ZGC 等。
- JClarity Censum:企业级 GC 分析工具。
- VisualVM/JMC:JVM 监控与堆分析。
2. 自动化调优建议
- 定期分析 GC 日志,检测 Full GC、碎片、并发失败等异常模式。
- 配合监控系统(Prometheus/Grafana)设置 GC 停顿时间、老年代使用率告警。
二十三、业务性能影响分析
- 延迟敏感业务(如 Web 服务、交易系统):CMS 可极大减少平均停顿时间,但碎片和 Full GC 可能导致偶发长时间卡顿。
- 吞吐优先业务(如批处理、大数据):CMS 并发线程争抢 CPU,可能影响整体吞吐,建议用 Parallel 或 G1。
- 高并发场景:CMS 的并发标记与清除对 CPU 资源消耗大,需合理配置线程数。
二十四、迁移到 G1/ZGC 的实战经验
1. 迁移步骤
- 评估业务需求:确定对延迟、吞吐、堆空间的要求。
- 切换 GC 参数:如
-XX:+UseG1GC或-XX:+UseZGC,并设置目标停顿时间。 - 调优分区和线程数:G1 可调
-XX:MaxGCPauseMillis, ZGC 可调-XX:ConcGCThreads。 - 监控与回归测试:观察 GC 日志,确认 Full GC、停顿时间、吞吐率是否达标。
2. 迁移坑点
- G1 的分区机制与 CMS 不同,参数需重新调优。
- ZGC 需 JDK11+,部分应用和监控工具可能不兼容。
- 业务高峰期需重点关注 GC 停顿分布,避免偶发长时间 STW。
二十五、生产事故排查经验
- 碎片导致 OOM:建议定期 Full GC 压缩、或提前迁移到 G1。
- 并发失败频繁:调低 CMS 启动阈值,增加并发线程,优化对象分配。
- GC日志丢失信息:确保日志参数完整,定期归档分析。
- 业务高峰卡顿:GC线程与业务线程合理分配,避免资源抢占。
二十六、总结
CMS 作为经典低延迟 GC,适合响应敏感但堆空间不极大的服务。其并发标记、写屏障、卡表机制能有效减少停顿,但碎片和 Full GC 是其最大短板。生产环境需结合业务场景、内存分配、GC日志持续调优,必要时迁移到 G1/ZGC。合理监控与自动化分析,是保障系统稳定的关键。
176万+

被折叠的 条评论
为什么被折叠?



