2275 字
约 7 分钟
14
S05-X14 · CMS 与 G1:两代收集器的机制与退役原因
2024-12-31
2026-11-08

老系统升级 JDK 17 时最常遇到的一行报错是 Unrecognized VM option 'UseConcMarkSweepGC'。CMS 从 JDK 9 起被标记弃用、JDK 14 正式移除,但"为什么被移除"这个问题比"怎么迁移"更值得搞清:CMS 的三个结构性问题至今仍在影响 G1 的调参直觉。


1. CMS 的四个阶段

阶段 是否 STW 做什么 关键点
初始标记 是(短) 标记 GC Roots 直接可达对象 依赖 Young GC 的停顿,速度快
并发标记 否 三色标记遍历对象图 与业务线程并发,需写屏障维护
重新标记 是(较长) 修正并发期间的引用变化 处理卡表与新生代引用,停顿敏感
并发清除 否 清扫未标记对象 不压缩,产生碎片

两个 STW 点是初始标记与重新标记,其中重新标记是最容易出问题的:它要重扫并发期间所有被修改的引用,堆越大、业务越活跃,这一步越久。

# CMS 时代的典型参数(JDK 8)
$ java -XX:+UseConcMarkSweepGC \
       -XX:CMSInitiatingOccupancyFraction=70 \
       -XX:+UseCMSInitiatingOccupancyOnly \
       -XX:CMSInitiatingPermOccupancyFraction=80 \
       -XX:+CMSScavengeBeforeRemark \
       -XX:ParallelGCThreads=8 -Xms4g -Xmx4g -jar app.jar

2. CMS 的三大问题

问题 机制 后果 相关参数
浮动垃圾 并发标记期间新产生的垃圾本轮标不到 本轮不回收,只能等下一轮 CMSInitiatingOccupancyFraction 调低留余量
并发模式失败 老年代增长快于并发清理,空间耗尽 退化为单线程 Full GC(Serial Old),长停顿 同上 + 提前触发
碎片 清除不压缩 大对象分配找不到连续空间 → promotion failed -XX:UseCMSCompactAtFullCollection(默认开)

"浮动垃圾"是设计上的必然代价:并发阶段不能把新分配对象全标一遍,只能下一轮再说。所以 CMS 必须提前启动,留出足够余量——这就是 CMSInitiatingOccupancyFraction 的本质:它不是调优参数,是安全边际。

Concurrent mode failure 的现场读法:

典型日志形态(CMS 时代输出截取):

[GC (CMS Initial Mark) ...]
[GC (CMS Final Remark) ...]
[Full GC (Allocation Failure) ... [CMS: 3.9G->3.8G]

出现 Full GC (Allocation Failure) 且伴随 CMS: old->old 几乎不降,说明并发清理没跑赢分配速率。此时先看 CMSInitiatingOccupancyFraction 是否太高(比如 92),再考虑堆是否真的不够。


3. 移除与迁移

版本 事件
JDK 9 CMS 被标记为 deprecated(JEP 是弃用通告)
JDK 14 CMS 正式移除(JEP 363)
JDK 17 无 CMS;G1 为默认;可选 ZGC / Shenandoah
JDK 21+ 分代 ZGC 出现

迁移不是把参数换个名字,而是目标函数变了:CMS 优化的是"降低单次停顿",G1 优化的是"在停顿目标内做取舍"。旧系统的低延迟诉求在 JDK 17 上应该先评估 G1 的 MaxGCPauseMillis,再考虑 ZGC。

$ java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms4g -Xmx4g -jar app.jar
$ java -XX:+UseZGC -Xms4g -Xmx4g -jar app.jar        # JDK 17 的 ZGC,单代
$ java -XX:+PrintGCDetails -Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=50m -jar app.jar

4. G1 的区域化堆与软目标

G1 把堆切成等大的 Region(默认按堆大小推导,-XX:G1HeapRegionSize 可指定,取值 1MB–32MB 的 2 的幂):

概念 含义 影响
Region 堆的逻辑分块,角色可变(Eden/Survivor/Old/Humongous) Region 数决定管理粒度
Collection Set 本轮要回收的 Region 集合 由停顿模型挑选
Remembered Set 记录跨 Region 引用 写屏障维护,有固定开销
MaxGCPauseMillis 软目标,不是硬承诺 收集器尽力而为

-XX:MaxGCPauseMillis 的软目标语义必须说清楚:G1 会据此估算"一轮能收多少个 Region",但当回收收益不足时它会放弃达标。如果堆里全是长寿对象,Region 回收后腾不出空间,G1 只能继续收更多 Region 或退化到 Full GC——这不是 bug,是目标函数冲突。

$ java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m \
       -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 \
       -XX:InitiatingHeapOccupancyPercent=45 -jar app.jar

IHOP(InitiatingHeapOccupancyPercent)是 G1 版的"提前启动"参数,对应 CMS 的 CMSInitiatingOccupancyFraction,但语义是"老年代占整堆比例达到此值就启动并发标记"。


5. G1 的三种失败

失败 触发条件 日志特征 处理
to-space exhausted Survivor 区不够,晋升失败 to-space exhausted 增大 SurvivorRatio / 提前晋升 / 降分配速率
Evacuation Failure 疏散时目标 Region 无空间 Evacuation Failure 同上看;检查 IHOP 是否过高
Humongous 分配 对象 ≥ Region 一半 Humongous 区域反复回收 增大 G1HeapRegionSize 或减少大对象

Humongous 的阈值是 RegionSize / 2。默认 Region 由堆大小推导,2G 堆上 Region 可能是 1MB,那么 512KB 以上的对象就是 Humongous——这在缓存批量加载、序列化大 payload 时非常容易触发。

$ java -Xlog:gc+humongous=debug -jar app.jar | head -20

典型输出形态(JDK 17 输出截取):

[0.512s][info][gc,humongous] GC(3) Humongous regions: 12->4

Humongous 对象在 Region 里连续存放,回收时必须整段释放,容易造成碎片与提前触发并发周期。这不是"内存不够",而是分配粒度与大对象不匹配。


6. CMS → G1 调参对照

CMS 参数 G1 对应 语义差异
CMSInitiatingOccupancyFraction InitiatingHeapOccupancyPercent CMS 是老年代占比,G1 是整堆占比
CMSScavengeBeforeRemark 无需对应 G1 的 Remark 前有并发清理,机制不同
CMSFullGCsBeforeCompaction 无需对应 G1 每轮都在疏散(复制),天然压缩
ParallelGCThreads ParallelGCThreads 同名,但 G1 另有 ConcGCThreads
-XX:+UseCMSCompactAtFullCollection 无需对应 见上
SurvivorRatio G1NewSizePercent / G1MaxNewSizePercent G1 用比例区间而非固定比例

这张表最实用的一条是:CMS 的"占用阈值"换到 G1 后数值不能照搬,因为分母从老年代变成了整堆。把 70 直接搬到 IHOP 通常意味着"启动得太晚"。


7. 常见坑

坑 现象 真实原因 处理
把 MaxGCPauseMillis 当硬承诺 设 50ms,实际 200ms 软目标,收益不足时放弃 调 Region 大小、减少长寿对象
大对象触发 Humongous 频繁回收 日志里 Humongous 反复出现 对象 ≥ Region/2 增大 Region 或拆对象
沿用 CMS 的占用阈值 G1 并发周期启动太晚 分母不同 按整堆比例重算,通常 40–50
以为 G1 没有 Full GC 出现 Pause Full (Allocation Failure) 疏散失败后退化 查 to-space/Evacuation Failure
关掉 IHOP 自适应 并发周期抖动 JDK 17 默认自适应 IHOP 谨慎显式设置
-XX:G1HeapRegionSize 设过大 小对象浪费空间 Region 太大导致内部碎片 按对象分布选 1–4MB

8. 排错

现象 原因 处理
Unrecognized VM option 'UseConcMarkSweepGC' JDK 14+ 已移除 删参数改 G1/ZGC
单次停顿远超目标 软目标未达成 看 -Xlog:gc 的 Pause Young 与 Region 数
频繁 Full GC 疏散失败或 IHOP 过晚 查 to-space exhausted,降 IHOP
老年代增长快但 GC 不频繁 分配速率高、年轻代过大 看分配速率与 G1NewSizePercent
迁移后吞吐下降 G1 写屏障与 RSet 开销 对比 -XX:ParallelGCThreads、评估 ZGC

写屏障这一项值得单独说:G1 的 RSet 维护在每次引用赋值上都有开销,这是它相对 CMS 的固定税。对写密集、对象图复杂的应用,这笔税可能吃掉 CMS 时代的吞吐优势——迁移后吞吐下降往往来自这里,而不是参数没调好。


9. 边界

场景 建议 理由
堆 < 4G、低延迟 G1 区域化管理粒度合适
堆 > 32G、极低延迟 ZGC G1 的 RSet 与疏散在大堆上代价上升
吞吐优先的批处理 Parallel GC 无并发开销,暂停换吞吐
小堆、短生命周期 Serial GC 开销最小
仍在 CMS 上的老系统 先在 JDK 17 上用 G1 跑基线 再决定是否上 ZGC

10. 自测

  1. CMS 的四个阶段里哪两个会 STW,为什么重新标记通常比初始标记更贵?
  2. 浮动垃圾是怎么产生的,它如何逼出 CMSInitiatingOccupancyFraction 这个参数?
  3. Concurrent mode failure 在日志里长什么样,退化后的收集器是什么?
  4. MaxGCPauseMillis 为什么是软目标?G1 在什么情况下会放弃达标?
  5. G1 的 Humongous 阈值怎么算,为什么大对象会引发提前回收?
  6. CMS 的占用阈值迁移到 G1 时为什么不能照搬数值?
S05-X14 · CMS 与 G1:两代收集器的机制与退役原因
https://www.clxhxhhr.top/posts/5765/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。