一、CMS 核心定位:什么是 CMS 收集器?
1. 核心定义
CMS 是 HotSpot 虚拟机中一款老年代垃圾收集器 ,基于 "标记 - 清除" 算法实现,核心特点是并发收集、低停顿------ 垃圾收集线程与用户线程大部分时间可并行执行,大幅减少 STW(Stop The World)时间,提升应用响应速度。
2. 通俗比喻
把 JVM 老年代比作大型商场 ,用户线程是购物的顾客 ,GC 线程是清洁工人:
- 其他收集器(如 Serial Old):清洁时清场,顾客全部暂停(STW),清洁完再让顾客进入,停顿时间长;
- CMS 收集器:清洁工人与顾客并行工作 ------ 先快速标记需要清洁的区域(短暂清场),然后一边让顾客购物,一边清理垃圾,仅在关键步骤短暂暂停顾客,停顿时间极短。
3. 适用场景与搭配
- 适用场景:响应时间优先的服务(如 Web 网站、API 接口、微服务),要求 GC 停顿时间控制在百毫秒内;
- 搭配收集器:CMS 仅负责老年代回收,新生代需搭配 ParNew 收集器(Serial 收集器的多线程版本),形成 "ParNew+CMS" 组合(JDK8 默认支持);
- 不适用场景:吞吐量优先的场景(如计算密集型任务)、大内存场景(如堆内存≥16G,CMS 性能会下降)。
二、CMS 核心原理:"标记 - 清除"+"并发执行"
1. 核心算法:标记 - 清除
CMS 基于 "标记 - 清除" 算法实现,分为 "标记" 和 "清除" 两个核心阶段:
- 标记阶段:识别老年代中存活的对象(通过可达性分析算法,以 GC Roots 为起点遍历);
- 清除阶段:回收未被标记的垃圾对象,释放内存空间。
但传统 "标记 - 清除" 算法存在效率低、产生内存碎片的问题,CMS 通过 "并发执行" 和 "分阶段优化" 解决了效率问题,却保留了内存碎片的缺陷(后续会详细说明)。
2. 核心设计:并发执行
CMS 的 "低停顿" 核心源于 "并发"------ 标记和清除阶段的大部分工作与用户线程并行执行,仅在初始标记和重新标记两个关键步骤触发短暂 STW。
这里要明确 "并发" 与 "并行" 的区别:
- 并发(Concurrent):GC 线程与用户线程同时运行(同一 CPU 核心交替执行);
- 并行(Parallel):多个 GC 线程同时运行(利用多核 CPU 并行处理,如 ParNew 收集器)。CMS 是 "并发收集器"(与用户线程并发),同时也是 "并行收集器"(多个 GC 线程并行执行标记 / 清除)。
三、CMS 完整工作流程:4 个阶段 + STW 关键点
CMS 的垃圾回收过程分为 4 个阶段,其中仅 2 个阶段需要 STW,且停顿时间极短(通常在几十毫秒内),另外 2 个阶段与用户线程并发执行,几乎不影响应用运行。
1. 阶段 1:初始标记(Initial Mark)------ STW(短暂)
核心工作
仅标记GC Roots 直接关联的对象(如虚拟机栈中引用的对象、静态变量引用的对象),不遍历整个引用链。
通俗理解
清洁工人先快速标记商场中 "入口处直接可见的垃圾"(如门口的塑料袋),不深入商场内部检查,因此速度极快。
特点
- STW 时间极短(通常 10ms 内),几乎无感知;
- 单线程执行(早期版本),后续版本支持多线程并行,进一步缩短停顿。
2. 阶段 2:并发标记(Concurrent Mark)------ 并发(无 STW)
核心工作
从初始标记的对象出发,遍历整个老年代的引用链,标记所有存活的对象。
通俗理解
清洁工人在顾客购物的同时,从门口的垃圾出发,逐一检查商场内部的每个角落,标记所有需要清理的垃圾,顾客正常购物不受影响。
特点
- 与用户线程并发执行,无 STW;
- 耗时最长(占整个 GC 周期的 80% 以上),但不影响应用响应;
- 可能产生 "浮动垃圾":并发标记期间,用户线程可能修改对象引用(如创建新对象、断开引用),导致部分垃圾未被标记(后续 GC 再清理)。
3. 阶段 3:重新标记(Remark)------ STW(短暂)
核心工作
修正并发标记期间因用户线程操作导致的标记偏差,重新标记被遗漏的存活对象。
通俗理解
清洁工人在顾客短暂暂停购物时,快速核对之前的标记结果,补充标记并发期间新增的存活对象(如顾客临时放下的物品),修正标记错误。
特点
- STW 时间比初始标记稍长(通常几十毫秒),但仍远短于 Full GC;
- 采用多线程并行执行,提升标记效率;
- 核心优化:使用 "增量更新"(Incremental Update)机制,处理并发标记期间的引用变化(具体原理见下文 "关键优化")。
4. 阶段 4:并发清除(Concurrent Sweep)------ 并发(无 STW)
核心工作
遍历老年代内存,回收所有未被标记的垃圾对象,释放内存空间。
通俗理解
清洁工人在顾客恢复购物后,清理所有之前标记的垃圾,顾客正常购物不受影响。
特点
- 与用户线程并发执行,无 STW;
- 不移动存活对象,因此会产生内存碎片(后续会讲解决方案);
- 回收过程中,用户线程可继续创建新对象,若老年代内存不足,会触发 "Concurrent Mode Failure"(并发模式失败),转而执行 Serial Old 收集器的 Full GC(停顿时间极长)。
总结:CMS 工作流程时序图
[初始标记(STW,10ms)] → [并发标记(无STW,数百ms)] → [重新标记(STW,50ms)] → [并发清除(无STW,数百ms)]
整个 GC 周期中,STW 总时间通常控制在 100ms 内,远优于 Serial Old(秒级停顿)和 Parallel Old(百毫秒~秒级停顿)。
四、CMS 的关键优化:增量更新(Incremental Update)
在并发标记阶段,用户线程可能修改对象引用(如黑色对象新增对白色对象的引用、灰色对象断开对白色对象的引用),导致 "漏标"(白色对象本是存活的却被标记为垃圾)。
CMS 通过 "增量更新" 机制解决漏标问题,核心逻辑:
- 当黑色对象(已标记且引用链遍历完成)新增对白色对象(未标记)的引用时,通过 "写屏障" 记录该引用关系;
- 重新标记阶段,以这些黑色对象为根,再次遍历引用链,补充标记遗漏的白色对象;
- 确保所有存活对象都被正确标记,避免 "错杀" 对象。
五、CMS 的优缺点:低停顿的代价
1. 优点(核心竞争力)
(1)低停顿(核心)
STW 时间极短,适合响应时间优先的场景,能显著提升用户体验(如 Web 应用接口响应更快、无明显卡顿)。
(2)并发执行
标记和清除阶段与用户线程并发,不占用过多 CPU 资源,对吞吐量影响较小。
(3)多线程并行
初始标记、重新标记阶段支持多线程并行,充分利用多核 CPU 优势,缩短 STW 时间。
2. 缺点(无法回避的问题)
(1)产生内存碎片
基于 "标记 - 清除" 算法,清除垃圾后会产生大量不连续的内存碎片。后果:
- 大对象无法分配连续内存,即使老年代总内存充足,也会触发 Full GC;
- 频繁的 Full GC 会导致应用卡顿,抵消低停顿的优势。
(2)并发模式失败(Concurrent Mode Failure)
并发清除阶段,用户线程持续创建新对象,若老年代内存快速被占满,CMS 无法及时回收,会触发 "并发模式失败",JVM 会自动切换到 Serial Old 收集器执行 Full GC(单线程、标记 - 整理算法),STW 时间极长(秒级)。
(3)占用额外 CPU 资源
并发标记和清除阶段,GC 线程与用户线程竞争 CPU 资源,会导致应用吞吐量下降(通常下降 10%~20%)。在单核 CPU 环境下,CMS 性能会严重退化。
(4)产生浮动垃圾
并发标记阶段,用户线程创建的新对象或修改引用产生的垃圾,无法在本次 GC 中回收,需等到下次 GC 清理,称为 "浮动垃圾"。浮动垃圾会占用老年代内存,增加并发模式失败的风险。
(5)不支持压缩(整理)
CMS 仅做 "标记 - 清除",不移动存活对象,无法解决内存碎片问题(后续会讲优化方案)。
六、CMS 实战调优:参数配置与问题解决
1. 核心 JVM 参数(启用与基础配置)
(1)启用 CMS 收集器
-XX:+UseConcMarkSweepGC # 启用CMS收集器(老年代)
-XX:+UseParNewGC # 新生代使用ParNew收集器(与CMS搭配)
启用后,JVM 默认新生代用 ParNew、老年代用 CMS,形成 "ParNew+CMS" 组合。
(2)内存相关参数
-Xms4g -Xmx4g # 堆内存初始/最大=4G
-Xmn2g # 新生代=2G(堆的50%),减少老年代压力
-XX:SurvivorRatio=8 # 新生代Eden:S0:S1=8:1:1(默认)
-XX:MaxTenuringThreshold=15 # 对象晋升老年代年龄阈值(默认15)
(3)CMS 核心调优参数
| 参数 | 作用 | 推荐配置 |
|---|---|---|
-XX:CMSInitiatingOccupancyFraction |
老年代使用率阈值,达到该值触发 CMS GC(默认 92%) | 80~85(提前触发,避免并发模式失败) |
-XX:+UseCMSInitiatingOccupancyOnly |
仅按上述阈值触发 CMS,不动态调整 | 启用(-XX:+UseCMSInitiatingOccupancyOnly) |
-XX:ParallelCMSThreads |
CMS 并发线程数(默认 = CPU 核心数 + 3)/4 | 按 CPU 核心数调整(如 8 核设为 2~4) |
-XX:+CMSFullGCsBeforeCompaction |
执行 N 次 Full GC 后进行一次内存整理(解决碎片) | 0~3(默认 0,即每次 Full GC 后整理) |
-XX:+CMSCompactAtFullCollection |
Full GC 时进行内存整理(压缩) | 启用(默认启用) |
-XX:CMSMaxAbortablePrecleanTime |
预清理阶段最大时间(默认 5 秒) | 5000ms(根据业务调整) |
2. 常见问题与解决方案
(1)并发模式失败(Concurrent Mode Failure)
现象 :日志中出现Concurrent Mode Failure,随后触发 Serial Old Full GC,停顿时间极长。
原因:
- 老年代使用率阈值设置过高(默认 92%),CMS 启动过晚,并发清除阶段内存不足;
- 新生代对象晋升过快,老年代被快速占满;
- 浮动垃圾过多,占用老年代内存。
解决方案:
- 降低
-XX:CMSInitiatingOccupancyFraction至 80~85,让 CMS 提前触发; - 增大老年代内存(如
-Xmx从 4G 调至 6G),或调整新生代比例(-Xmn),减少对象晋升; - 优化应用代码,减少大对象创建和长期存活对象。
(2)内存碎片过多
现象:老年代总内存充足,但频繁触发 Full GC,日志中显示 "老年代碎片率高"。
原因:CMS "标记 - 清除" 算法产生的内存碎片,导致大对象无法分配连续内存。
解决方案:
- 启用
-XX:+CMSCompactAtFullCollection(默认启用),Full GC 时自动整理内存; - 通过
-XX:CMSFullGCsBeforeCompaction=3,每执行 3 次 Full GC 后强制整理一次(平衡停顿时间和碎片率); - 避免创建过大的对象(如超过老年代内存的 10%),或通过
-XX:PretenureSizeThreshold设置大对象阈值,让大对象直接进入老年代(减少晋升压力)。
(3)重新标记阶段 STW 时间过长
现象:重新标记阶段 STW 时间超过 100ms,影响应用响应。
原因:
- 并发标记期间用户线程修改的引用过多,重新标记需要遍历大量对象;
- CMS 并发线程数不足,标记效率低。
解决方案:
- 增加
-XX:ParallelCMSThreads线程数(如 8 核 CPU 设为 4),提升并行标记效率; - 启用
-XX:+CMSScavengeBeforeRemark,重新标记前先执行一次 Minor GC,减少老年代引用的新生代对象,缩短标记时间; - 优化应用代码,减少并发标记期间的对象引用修改(如避免在高并发场景下频繁修改静态变量引用)。
3. CMS 日志分析示例
启用 CMS 后,GC 日志会包含如下关键信息(简化版):
# 初始标记(STW)
[GC (CMS Initial Mark) [1 CMS-initial-mark: 15360K(30720K)] 17920K(40960K), 0.0050000 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
# 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.2000000 secs] [Times: user=0.50 sys=0.10, real=0.20 secs]
# 重新标记(STW)
[GC (CMS Final Remark) [YG occupancy: 2560K (10240K)] [Rescan (parallel) , 0.0300000 secs] [Weak Ref Processing, 0.0010000 secs] [Class Unloading, 0.0020000 secs] [Scrub Symbol Table, 0.0030000 secs] [Scrub String Table, 0.0010000 secs] [1 CMS-remark: 15360K(30720K)] 17920K(40960K), 0.0370000 secs] [Times: user=0.10 sys=0.01, real=0.04 secs]
# 并发清除
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.1500000 secs] [Times: user=0.40 sys=0.08, real=0.15 secs]
CMS-initial-mark:初始标记,STW 时间 0.01 秒;CMS-concurrent-mark:并发标记,耗时 0.2 秒(无 STW);CMS Final Remark:重新标记,STW 时间 0.04 秒;CMS-concurrent-sweep:并发清除,耗时 0.15 秒(无 STW);- 总 STW 时间仅 0.05 秒,符合低停顿目标。
七、CMS 与 G1 收集器的对比:为什么 CMS 会被淘汰?
JDK9 后 G1 取代 CMS 成为默认收集器,核心原因是 G1 解决了 CMS 的诸多缺陷,同时保留了低停顿优势。两者关键对比:
| 对比项 | CMS 收集器 | G1 收集器 |
|---|---|---|
| 核心算法 | 标记 - 清除(老年代) | 标记 - 整理(局部复制,全局整理) |
| 内存碎片 | 有(严重) | 无(内存连续) |
| 停顿可预测性 | 不可预测 | 可预测(通过-XX:MaxGCPauseMillis设置) |
| 内存布局 | 新生代 + 老年代(物理隔离) | Region 分区(逻辑分代,物理不隔离) |
| 并发模式失败 | 易触发(切换 Serial Old) | 不易触发(动态调整 Region) |
| 大内存支持 | 差(堆≥16G 性能下降) | 好(堆≥32G 仍高效) |
| 吞吐量 | 中等(并发占用 CPU) | 较高(优化的并行执行) |
结论:
- 若使用 JDK9+,优先选择 G1 收集器,无需纠结 CMS;
- 若维护 JDK8 及以前的老系统,且对响应时间要求高,可使用 CMS,但需做好调优(避免并发模式失败和内存碎片);
- 计算密集型、吞吐量优先的场景,推荐使用 Parallel Scavenge+Parallel Old 组合,而非 CMS。