CMS(Concurrent Mark Sweep)已经在 JDK 9 中被标记为 Deprecate(废弃),并在 JDK 14 中被彻底移除。
但在大量使用 JDK 8 的老牌公司(银行、电信、传统软件)中,CMS 依然在生产环境运行。
一、 CMS 核心工作原理(为什么要这样配?)
CMS 的目标是低延迟,它的 GC 线程是和业务线程并发执行的。但它有两个致命弱点:
浮动垃圾:并发清理时产生的垃圾只能下次再清。
内存碎片:它使用"标记-清除"算法,不进行压缩(除非 Full GC)。
因此,配置 CMS 的核心思路是:预留足够的空间防止并发失败,并定期整理碎片。
二、 JDK 8 + CMS 推荐配置模板
bash
# ===== 基础内存设置 =====
-Xms4g -Xmx4g # 堆内存固定,防止动态扩容抖动
-Xmn1536m # 强制设置年轻代大小(CMS通常配合固定年轻代)
-Xss256k # 线程栈
# ===== 选择垃圾回收器 =====
-XX:+UseConcMarkSweepGC # 启用 CMS(这是必须的开关)
-XX:+UseParNewGC # 年轻代使用 ParNew(配合 CMS 的多线程收集器)
# 注意:在 JDK 8 中,指定 UseConcMarkSweepGC 通常会隐式启用 UseParNewGC
# ===== CMS 并发阶段调优(核心)=====
# 1. 触发阈值(最关键)
# 当老年代使用率达到 70% 时,启动 CMS 并发周期。
# 默认是 92%。设低一点,给业务线程留出缓冲空间,防止并发失败。
-XX:CMSInitiatingOccupancyFraction=70
# 允许 JVM 动态调整阈值(配合上面的参数使用,建议开启)
-XX:+UseCMSInitiatingOccupancyOnly
# 2. 并发线程数
# 并发标记阶段的线程数。公式:(CPU核数 + 3) / 4。
# 如果 CPU 负载高,可以减少;如果 GC 跟不上分配,可以增加。
-XX:ConcGCThreads=2
# 年轻代回收时的并行线程数(通常与 CPU 核数一致)
-XX:ParallelGCThreads=4
# 3. 内存碎片整理(保命参数)
# 开启在 Full GC 时进行内存碎片的压缩整理(默认开启,但必须显式确认)
-XX:+UseCMSCompactAtFullCollection
# 设置多少次 Full GC 进行一次压缩(默认是 0,即每次都压)。
# 设置为 5 表示每 5 次 Full GC 整理一次。平衡停顿时间和碎片。
-XX:CMSFullGCsBeforeCompaction=5
# ===== 类卸载与元空间 =====
# 开启类卸载(防止 PermGen/Metaspace 泄漏)
-XX:+CMSClassUnloadingEnabled
# 设置元空间大小(JDK 8 取代了 PermGen)
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
# ===== 日志与诊断 =====
# JDK 8 经典 GC 日志配置
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution # 打印对象年龄分布(调优 Survivor 很有用)
-XX:+PrintHeapAtGC # 打印 GC 前后堆详情
-Xloggc:/var/log/gc.log
# OOM 处理
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
# ===== 性能优化(可选)=====
# 禁用显式 GC(如 System.gc()),但 NIO/Direct Memory 依赖它,需谨慎
# -XX:+DisableExplicitGC
# 偏向锁优化(高并发下关闭可能更好,单线程下开启)
-XX:+UseBiasedLocking
三、 CMS 参数深度解析(避坑指南)
- 关于 CMSInitiatingOccupancyFraction
这是 CMS 最容易出错的参数。
现象:如果你设为 92%(默认),业务线程疯狂创建对象,老年代涨到了 93%,而 CMS 还没清理完。这时 JVM 会放弃并发模式,冻结业务线程,启动 Serial Old GC(单线程回收老年代)。
后果:长时间的 Stop-The-World(几秒甚至几十秒)。
对策:一定要配合 -XX:+UseCMSInitiatingOccupancyOnly 使用。否则 JVM 会忽略你设定的 70%,自己跑 heuristic(启发式)算法,导致参数失效。
- 关于 UseCMSCompactAtFullCollection
CMS 是"标记-清除",会产生碎片。
当分配一个大对象找不到连续空间时,会触发 Full GC。
这个参数保证了在 Full GC 时会进行压缩整理(像整理磁盘碎片一样),虽然停顿时间长,但能腾出连续空间。
注意:G1 不需要这个参数,因为它天然会整理。
- 关于 -Xmn (年轻代大小)
不同于 G1 的动态调节,使用 CMS 时通常建议显式设置年轻代大小 (-Xmn)。
原因:如果年轻代太小,Minor GC 会很频繁;如果太大,会导致老年代变小,容易触发 CMS。
经验值:通常设置为堆大小的 1/3 到 1/2。例如 4G 堆,-Xmn1536m 到 -Xmn2048m。
四、 CMS 常见故障排查
如果在日志中看到以下内容,说明 CMS 出问题了:
concurrent mode failure
原因:CMS 还在并发清理,老年代就满了。
解决:降低 CMSInitiatingOccupancyFraction(如从 70 降到 60),或者增加堆大小,或者减少 CMSMaxAbortablePrecleanTime。
promotion failed
原因:Minor GC 时,Survivor 区放不下,老年代也没有足够的连续空间(碎片化严重)来担保晋升。
解决:增大 Survivor 区(调整 -XX:SurvivorRatio,如改为 8 或 6),或者开启碎片整理,或者增加堆大小。
频繁的 Full GC,但老年代还有很多空余
原因:可能是 CMSInitiatingOccupancyFraction 设置过低,或者 Metaspace 满了触发了 GC。
解决:检查 Metaspace 日志,或者调整阈值。
五、 CMS vs G1:你应该怎么选?
既然你之前问了 G1,这里做一个直接的对比,帮助你决策:

我的强烈建议:
除非你的应用对停顿极度敏感(且你已经是非常资深的 JVM 专家),或者因为某些兼容性原因必须使用 JDK 8 且无法切换到 G1,否则请尽量从 CMS 迁移到 G1。
G1 在 JDK 8u60 之后的版本已经非常稳定,且不需要你像照顾 CMS 那样小心翼翼地去调整 CMSInitiatingOccupancyFraction 这种复杂的参数。
模拟的 GC 日志片段
假设你的 JVM 参数是:-Xmx4g -Xms4g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70
bash
# 1. 正常的 Young GC (Minor GC)
2026-07-27T14:30:01.234+0800: 125.678: [GC (Allocation Failure) 2026-07-27T14:30:01.235+0800: 125.679: [ParNew: 1385472K->153600K(1572864K), 0.0567890 secs] 2147483K->1958400K(4128768K), 0.0571234 secs] [Times: user=0.23 sys=0.01, real=0.06 secs]
# 2. CMS 并发周期开始(因为老年代使用率达到了 70% 的阈值)
2026-07-27T14:30:02.100+0800: 126.544: [GC (CMS Initial Mark) [1 CMS-initial-mark: 1804800K(2555904K)] 1960000K(4128768K), 0.0123456 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2026-07-27T14:30:02.113+0800: 126.557: [CMS-concurrent-mark-start]
2026-07-27T14:30:02.456+0800: 126.900: [CMS-concurrent-mark: 0.343/0.343 secs] [Times: user=1.20 sys=0.03, real=0.34 secs]
2026-07-27T14:30:02.457+0800: 126.901: [CMS-concurrent-preclean-start]
2026-07-27T14:30:02.501+0800: 126.945: [CMS-concurrent-preclean: 0.044/0.044 secs] [Times: user=0.08 sys=0.01, real=0.04 secs]
2026-07-27T14:30:02.502+0800: 126.946: [CMS-concurrent-abortable-preclean-start]
# 3. 在 CMS 还没清理完的时候,业务线程继续分配对象,又触发了一次 Young GC
2026-07-27T14:30:03.300+0800: 127.744: [GC (Allocation Failure) 2026-07-27T14:30:03.301+0800: 127.745: [ParNew: 1538560K->153600K(1572864K), 0.0456789 secs] 3343360K->3155200K(4128768K), 0.0460123 secs] [Times: user=0.18 sys=0.01, real=0.05 secs]
# 4. 【死亡时刻】CMS 还在并发工作,但老年代被填满了!
2026-07-27T14:30:03.350+0800: 127.794: [GC (Allocation Failure) 2026-07-27T14:30:03.351+0800: 127.795: [ParNew: 1538560K->1538560K(1572864K), 0.0000456 secs] 3155200K->3155200K(4128768K), 0.0000789 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
# 5. 【核心标志】Concurrent Mode Failure!
2026-07-27T14:30:03.352+0800: 127.796: [CMS2026-07-27T14:30:03.353+0800: 127.797: [CMS-concurrent-abortable-preclean: 0.851/1.234 secs] [Times: user=2.50 sys=0.10, real=1.23 secs]
(concurrent mode failure): 3001600K->2555903K(2555904K), 2.5678901 secs] 3155200K->2750000K(4128768K), [Metaspace: 128456K->128456K(1310720K)], 2.5689123 secs] [Times: user=2.57 sys=0.00, real=2.57 secs]
# 6. 后续恢复正常的 Young GC
2026-07-27T14:30:06.000+0800: 130.444: [GC (Allocation Failure) 2026-07-27T14:30:06.001+0800: 130.445: [ParNew: 314572K->34956K(1572864K), 0.0234567 secs] 3064572K->2789456K(4128768K), 0.0237890 secs] [Times: user=0.09 sys=0.00, real=0.02 secs]
深度解析:如何像侦探一样阅读这段日志?
第一步:找到"案发现场"的标志性关键词
在这一行,你看到了 JVM 抛出的"红色警报":
bash
(concurrent mode failure)
只要看到这个,就可以断定:CMS 崩了。
第二步:理解发生了什么(故事还原)
我们把日志的时间线串起来:
-
14:30:02.113:CMS 开始了并发标记周期(CMS Initial Mark)。此时老年代用了 1.8G(共
2.5G),刚刚达到 70% 的阈值。
-
14:30:02 到 14:30:03:CMS 正在后台慢慢悠悠地标记垃圾(和业务线程并发)。
-
14:30:03.300:业务线程继续疯狂创建对象,触发了一次 Young GC。这次 GC 试图把对象从 Eden 区晋升到老年代。
-
14:30:03.350:灾难发生。由于业务创建对象太快,老年代在 CMS 还没来得及清理之前就被塞满了(3001600K /
2555904K,几乎 100%)。
-
14:30:03.352:JVM 不得不中断正在进行的 abortable-preclean 阶段,并启动 Serial Old
GC(单线程回收)。这就是为什么你会看到那行 2.5678901 secs 的停顿------长达 2.56 秒!
第三步:识别关键特征(考试重点)
在日志中识别 concurrent mode failure,请认准这三个特征:
特征 1:前后紧接的两次 GC
你会看到一次普通的 Young GC(ParNew)紧接着一次包含 (concurrent mode failure) 的 GC,中间几乎没有时间间隔。
正常情况:GC 之间有业务运行的时间空隙。
故障情况:0.0460123 secs] 刚结束,马上就是 0.0000456 secs]。
特征 2:晋升失败(Promotion Failed)的前兆
在 Failure 之前,你通常会看到 ParNew 的日志是这样的:
bash
[ParNew: 1538560K->1538560K(1572864K), 0.0000456 secs]
注意看箭头 ->:前面是 1538560K,后面还是 1538560K。
这说明 Young GC 试图回收 Eden,结果发现 Survivor 区放不下,想往老年代塞,结果老年代也没地方了(或者因为碎片没连续空间)。于是这次 Young GC 白干了,对象一点没挪走。
特征 3:漫长的停顿时间
real=2.57 secs。
在 CMS 模式下,正常的 Young GC 通常在几十毫秒,正常的 CMS 并发阶段几乎不暂停。一旦出现秒级的停顿,99% 是 concurrent mode failure 或者 promotion failed 导致的 Full GC。
解决方案(对症下药)
当你在日志中发现了 (concurrent mode failure),应该怎么修?
降低阈值(最常见):
既然老年代涨到 70% 才开始清理来不及,那就早点开始。
bash
# 原来是 70,改成 60 或更低
-XX:CMSInitiatingOccupancyFraction=60
注意:必须配合 -XX:+UseCMSInitiatingOccupancyOnly 使用,否则 JVM 会忽略你的设置。
增加堆内存或减少对象创建:
如果业务峰值流量确实太高,CMS 无论如何都赶不上分配速度,只能加内存(-Xmx)或者优化代码(减少大对象、缓存复用)。
调整碎片整理:
如果是因为内存碎片导致明明有空间却放不下大对象,需要增加整理频率。```
```bash
-XX:CMSFullGCsBeforeCompaction=0 # 每次 Full GC 都整理(会增加停顿,但解决碎片)