CMS 回收器(原理/特点/参数/弊端)
CMS:第一个并发收集器
CMS(Concurrent Mark Sweep) 是 Java 第一个实现并发标记的垃圾回收器。它的核心目标是:减少 GC 停顿时间。CMS 让大部分 GC 工作与用户线程并发执行,从而大幅缩短停顿时间。
CMS 的工作原理
CMS 的工作过程分为六个阶段,其中只有初始标记和重新标记需要 STW:
CMS 执行阶段:
┌──────────────────────────────────────────────────────┐
│ │
│ 1. 初始标记(Initial Mark)── STW ──▶ 短暂停顿 │
│ 标记 GC Roots 直接引用的对象 │
│ │
│ 2. 并发标记(Concurrent Mark)─────────▶ 并发执行 │
│ 从 GC Roots 出发,追踪所有存活对象 │
│ (用户线程同时运行) │
│ │
│ 3. 重新标记(Remark)────────── STW ──▶ 修正停顿 │
│ 修正并发标记期间产生的变化 │
│ │
│ 4. 并发清除(Concurrent Sweep)────────▶ 并发执行 │
│ 清除所有未标记的垃圾对象 │
│ (用户线程同时运行) │
│ │
│ 5. 并发重置(Concurrent Reset)────────▶ 并发执行 │
│ 重置 CMS 的内部状态 │
│ │
└──────────────────────────────────────────────────────┘五阶段的详细解析
阶段一:初始标记(Initial Mark)
java
// 场景:CMS 开始标记
// GC Roots:
// - 静态变量引用
// - 线程栈引用
// - JNI 引用
// 初始标记只标记 GC Roots 直接引用的对象(一级引用)
// 停顿时间很短(通常几十毫秒)- 是否 STW:是
- 停顿时间:短暂
- 标记内容:GC Roots 直接引用的对象
阶段二:并发标记(Concurrent Mark)
用户线程同时运行:────────────────────────▶ 时间
│
CMS 线程并发标记:──▶ 标记存活对象 ─────────▶
│
从标记的对象继续追踪
二级、三级引用...
注意:并发标记期间,用户线程可能修改引用关系- 是否 STW:否(并发执行)
- 停顿时间:无(但消耗 CPU)
- 标记内容:从 GC Roots 出发的所有存活对象
阶段三:重新标记(Remark)
并发标记期间,用户线程可能修改了引用关系:
并发标记时,用户线程把对象 B 的引用改成了对象 C:
Old Obj ──▶ 对象 B ──▶ 被标记为存活
│
Old Obj ──▶ 对象 C ──▶ 未被标记(但应该存活!)
重新标记修正这个问题:把所有在并发期间
「指向存活对象却被漏标」的对象重新标记- 是否 STW:是
- 停顿时间:比初始标记长(需要扫描变化区域)
- 优化:使用 增量更新(Incremental Update) 减少停顿
阶段四:并发清除(Concurrent Sweep)
CMS 线程并发扫描老年代,标记垃圾对象:
┌─────────────────────────────────────────┐
│ [存活] [垃圾] [存活] [垃圾] [存活] │
│ ✓ ✓ ✓ │
└─────────────────────────────────────────┘
清除垃圾,释放内存- 是否 STW:否(并发执行)
- 特点:不整理内存,会产生碎片
阶段五:并发重置(Concurrent Reset)
重置 CMS 的内部数据结构,为下一次 GC 做准备。
CMS 的使用
bash
# 启用 CMS 收集器
java -XX:+UseConcMarkSweepGC MyApp
# 年轻代自动使用 ParNew
# -XX:+UseParNewGC 是默认行为(可省略)CMS 的参数配置
bash
# 触发老年代并发收集的堆使用率阈值
# 默认 68%,即堆使用到 68% 时开始并发标记
java -XX:CMSInitiatingOccupancyFraction=75 MyApp
# 强制在 FullGC 时使用 Serial Old(备选方案)
java -XX:+UseCMSCompactAtFullCollection MyApp
# JDK 9+ 已默认开启
# 设置多少次 FullGC 后压缩老年代
java -XX:CMSFullGCsBeforeCompaction=5 MyApp
# 启用增量模式(减少并发占用的 CPU)
# 适合 CPU 资源有限的场景
java -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode MyAppCMS 的四大弊端
弊端一:CPU 敏感
CMS 在并发阶段需要消耗 CPU 资源:
| 阶段 | 是否 STW | CPU 占用 |
|---|---|---|
| 初始标记 | 是 | N/A(很短) |
| 并发标记 | 否 | 1 个 CPU 核心 |
| 重新标记 | 是 | N/A |
| 并发清除 | 否 | 1 个 CPU 核心 |
如果机器 CPU 核心数少(< 4),CMS 可能导致性能下降。
弊端二:内存碎片
CMS 使用标记-清除算法,不整理内存:
CMS 并发清除后:
┌─────────────────────────────────────────────┐
│ [存活] [洞洞] [存活] [洞洞洞洞] [存活] │
│ 碎片 大量碎片 碎片 │
└─────────────────────────────────────────────┘碎片积累后,即使总空闲内存够用,也可能无法分配大对象。
弊端三:浮动垃圾
并发标记期间产生的「浮动垃圾」必须等到下一次 GC 才能回收:
浮动垃圾场景:
1. 并发标记期间,对象 X 原本被标记为存活
2. 标记完成后,用户线程把 X 的引用清空
3. X 现在变成了垃圾,但已经「标记完成」
4. 必须等到下一次 GC 才能回收 X弊端四:并发模式失败(Concurrent Mode Failure)
最严重的问题:并发模式失败。
并发模式失败场景:
CMS 并发执行中...
│
│ 老年代满了,但 CMS 无法完成并发收集
│ (并发期间有太多新对象晋升到老年代)
│
▼
JVM 紧急切换到 Serial Old(MSC)
│
│ Serial Old 是单线程标记-压缩
│ 停顿时间可能达到几秒!
│
▼
程序长时间停顿,性能急剧下降CMS 的演进与淘汰
CMS 历史:
2014 ──→ JDK 9:CMS 标记为废弃
2017 ──→ JDK 9:G1 成为默认 GC
2018 ──→ JDK 14:CMS 正式移除替代方案
| 替代 | 说明 |
|---|---|
| G1 | JDK 9 默认,平衡吞吐和延迟 |
| ZGC | JDK 11+,亚毫秒级停顿 |
| Shenandoah | JDK 12+,类似 ZGC |
CMS 的 GC 日志
bash
java -XX:+UseConcMarkSweepGC \
-XX:+PrintGCDetails \
MyApp日志示例:
# 初始标记
2026-03-22T10:00:00.000: [GC (CMS Initial Mark)
[CMS-initial-mark: 1048576K/2097152K]
1048576K->1048576K(2097152K), 0.0123456 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
# 并发标记
# (并发阶段没有日志)
# 重新标记
2026-03-22T10:00:00.123: [GC (CMS Final Remark)
[CMS-remark: 1048576K->1048576K(2097152K)]
0.0234567 secs]
[Times: user=0.03 sys=0.01, real=0.02 secs]
# 并发清除
# (并发阶段没有日志)本节小结
CMS 收集器的核心要点:
| 维度 | 说明 |
|---|---|
| 设计目标 | 低停顿(并发收集) |
| 算法 | 标记-清除 |
| 阶段 | 初始标记 → 并发标记 → 重新标记 → 并发清除 |
| 停顿时间 | 仅初始标记 + 重新标记(短暂) |
| 弊端 | CPU 敏感、内存碎片、浮动垃圾、并发失败 |
| JDK 状态 | JDK 14 移除 |
CMS 是 JVM GC 历史上的里程碑——它是第一个实现并发收集的回收器。但它的缺陷(碎片、并发失败)最终导致它被 G1 和 ZGC 取代。
下一节,我们来看现代 GC 的代表——G1 回收器(核心)。
