CMS:已被移除的并发收集器(JDK 8→升级必读)#
版本信息
如果你维护的 JDK 8 服务还配着
-XX:+UseConcMarkSweepGC,升级到 JDK 14+ 会直接启动失败。这一页回答三件事:CMS 当初为什么受欢迎、它为什么被干掉、升级时该换成什么。
设计目标:降低老年代回收停顿#
在 CMS 之前,老年代回收(Full GC)是全 STW的整堆操作,停顿随堆增长。CMS(JDK 5 引入)首次把老年代的大部分回收工作与应用线程并发进行,显著降低了单次停顿——在那个年代,它是低延迟的代名词。但它从未成为默认,需手动 -XX:+UseConcMarkSweepGC 启用。
为什么被移除#
CMS 是 HotSpot 历史上最复杂的 GC之一,被淘汰有三个硬原因:
- 碎片化(致命伤):CMS 用标记-清除(mark-sweep),不整理老年代。长期运行后老年代布满碎片,没有足够连续空间容纳晋升对象 → 触发
promotion failure;或老年代在 CMS 周期跑完前就满了 →concurrent mode failure。两者都会退化成 Serial Old 的整堆 Full GC——停顿比它要降低的还长得多,彻底违背低停顿初衷。 - 代码复杂、维护成本高:CMS 的并发标记、屏障、 Remembered Set 与新 GC(G1/ZGC/Shenandoah)的并发基础设施大量重复且互相耦合,成了 HotSpot 的维护包袱。
- G1 已成熟可替代:G1 在 JDK 9 成为默认,region 化 + 整理既停顿可控又无碎片化,足以替代 CMS 的绝大多数场景。CMS 失去了存在意义。
连带移除:CMS 的年轻代收集器 ParNew(
-XX:+UseParNewGC)只能和 CMS 搭配,CMS 移除后它也在 JDK 14 一同消失。
工作原理:六阶段,大部分并发#
graph TD
IM["① 初始标记 STW<br/>标记 GC 根直接引用"] --> CM["② 并发标记<br/>与应用线程同时跑"]
CM --> PC["③ 预清理(并发)"]
PC --> AP["④ 可中止预清理(并发)"]
AP --> RM["⑤ 重新标记 STW<br/>修正并发期的引用变化"]
RM --> SW["⑥ 并发清除<br/>回收不可达对象"]
SW --> F{"碎片化 / 空间不足?"}
F -->|"否(多数周期)"| OK["正常运行<br/>进入下一周期"]
F -->|"promotion failure / concurrent mode failure"| FF["退化:Serial Old Full GC<br/>长停顿,整堆整理"]
style IM fill:#ffe0b2
style RM fill:#ffe0b2
style OK fill:#c8e6c9
style FF fill:#ffcdd2
只有 ① 初始标记 和 ⑤ 重新标记 两个短点是 STW,其余 ②③④⑥ 都与应用并发——这是 CMS 低停顿的来源。但代价是 ⑥ 不整理 → 碎片化 → 退化的长 Full GC(图右路径)。
JVM 配置参数(仅 JDK ≤13 有效)#
| 参数 | 含义 | 默认 | 备注 |
|---|---|---|---|
-XX:+UseConcMarkSweepGC |
启用 CMS(年轻代用 ParNew) | 关(非默认) | JDK 14 起已移除,启用即报错 |
-XX:CMSInitiatingOccupancyFraction=<n> |
老年代占用百分比达 n% 触发 CMS 周期 |
自适应(≈92%) | 调小让 CMS 更早启动,缓解空间压力 |
-XX:+UseCMSInitiatingOccupancyOnly |
禁用自适应,固定用上面百分比 | 关 | 让触发时机可预测 |
-XX:+CMSParallelRemarkEnabled |
重新标记阶段并行化 | — | 缩短 ⑤ 的 STW |
-XX:+CMSScavengeBeforeRemark |
重新标记前先做一次年轻代 GC | — | 减少跨代引用扫描,缩短 ⑤ |
-XX:CMSMaxAbortablePrecleanTime=<ms> |
可中止预清理的最长等待 | 5000 | 控制何时进入重新标记 |
# 仅 JDK 8/9~13 有效;JDK 14+ 会报 Unrecognized VM option
java -XX:+UseConcMarkSweepGC -Xmx4g \
-XX:CMSInitiatingOccupancyFraction=70 \
-XX:+UseCMSInitiatingOccupancyOnly \
-XX:+CMSScavengeBeforeRemark \
-jar app.jar
升级怎么办:换成 G1 或 ZGC#
CMS 已无继任意义上的「同款」——直接按需求换:
| 原 CMS 诉求 | 现代替代 | 启用 |
|---|---|---|
| 「停顿可控、通用大堆」 | G1(默认) | 不用配,或 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 |
| 「更极致的低延迟、大堆」 | ZGC | -XX:+UseZGC -Xmx16g |
| 「低延迟 + Red Hat 系发行版」 | Shenandoah | -XX:+UseShenandoahGC |
实操:升级前先把所有
-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC及 CMS 专属调优标志全部删掉,让 JVM 用默认 G1;再按停顿/吞吐实测决定是否上 ZGC。
与前代 GC 对比:Parallel → CMS(STW → 老年代并发)#
CMS 相对当时的默认 Parallel,把老年代回收并发化——Parallel 的 Full GC 是全 STW 整堆、停顿随堆增长;CMS 把大部分老年代回收放到应用线程并发执行,停顿显著降低。代价是不整理 → 碎片化,最终被 G1 取代并移除。
java -XX:+UseParallelGC -Xmx8g -jar app.jar # 老年代 Full GC 整堆 STW,停顿长
java -XX:+UseConcMarkSweepGC -Xmx4g -jar app.jar # 老年代并发、停顿短,但不整理→碎片化
CMS 生命周期:JDK 8 仍可用 → JDK 9(JEP 291)弃用 → JDK 14(JEP 363)移除(
-XX:+UseConcMarkSweepGC报Unrecognized VM option)。它换来的低停顿被 G1 / ZGC 更好地继承。
常见坑 / 最佳实践#
- 碎片化是 CMS 的本质问题,调参只能延缓:
-XX:CMSInitiatingOccupancyFraction调小能让 CMS 更早跑、减少空间压力,但治标不治本,长期仍会碎片化并退化。 - 升级脚本务必清理 CMS 标志:JDK 14+ 启动会直接失败;别等到生产报错才发现。
- CMS 的「低停顿」是相对时代的:相比今天的 ZGC 亚毫秒停顿,CMS 的停顿毫秒到几十毫秒,且退化时反而很长。
- ParNew 也没了:
-XX:+UseParNewGC与 CMS 同命运,JDK 14 起同样Unrecognized。 - 别在 JDK 8 用 CMS 的脚本硬迁:CMS 的调优标志(
CMSInitiatingOccupancyFraction等)在 G1/ZGC 上无效甚至有害,迁移时一并清除,重新按新 GC 的语义调。
小结#
CMS 是 Java 走向并发低延迟回收的先驱,功不可没;但「标记-清除不整理 → 碎片化 → 退化长 Full GC」是它的结构性死穴,加之维护成本高、G1 已成熟,最终在 JDK 14 被移除。还在用 CMS 的 JDK 8 服务,升级时请果断切到 G1(默认)或 ZGC。