news 2026/10/1 8:35:02

JVM CMS 垃圾回收器深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM CMS 垃圾回收器深度解析

JVM CMS 垃圾回收器深度解析

CMS(Concurrent Mark Sweep,并发标记清除)是 JVM 历史上第一款真正意义上的低停顿老年代垃圾回收器,从 JDK 1.4 引入,JDK 9 被标记为 Deprecated,JDK 14 正式移除。虽然它已退出历史舞台,但其设计思想(并发回收、分阶段标记)直接影响了后来的 G1、ZGC、Shenandoah,同时也是高级 Java 面试的高频考点。


一、CMS 的设计目标

在 CMS 出现之前,老年代回收主要依靠 Serial Old(单线程、STW)和 Parallel Old(多线程、STW)。这两者的停顿时间与堆大小正相关——堆越大,停顿越长。

CMS 的核心目标是:将大部分回收工作放到与用户线程并发执行的阶段,把 STW 时间压缩到毫秒级,且停顿时间不随堆大小线性增长。

这个目标决定了它的两个基本策略:

  1. 并发(Concurrent):标记和清除阶段与业务线程同时运行
  2. 标记-清除(Mark-Sweep):不采用复制/整理算法,避免移动对象带来的长时间停顿

二、回收的四个阶段

CMS 的一个完整回收周期分为四个阶段,其中两个 STW、两个并发:
下面是 CMS 完整回收周期的 Mermaid 流程图,标注了各阶段的 STW 情况,以及并发失败(Concurrent Mode Failure)时回退到 Serial Old 的路径:

是

否(Concurrent Mode Failure)

CMS 回收周期开始

初始标记
Initial Mark(STW)

并发标记
Concurrent Mark(与业务线程并发)

并发期间
预留空间是否足够?

重新标记
Remark(STW)

回退 Serial Old
单线程整理式 Full GC(长停顿)

并发清除
Concurrent Sweep(与业务线程并发)

回收周期结束

说明:图中红色节点(初始标记、重新标记)为 STW 阶段,绿色节点(并发标记、并发清除)与业务线程并发执行;当并发标记期间预留空间不足时,CMS 会触发 Concurrent Mode Failure,回退到 Serial Old 单线程整理式 Full GC,停顿可能达到数秒。

2.1 初始标记(Initial Mark)—— STW

  • 只标记 GC Roots 直接关联的对象(老年代中被新生代对象引用的对象、被栈/本地方法区引用的对象)
  • 虽然是 STW,但由于不做整堆遍历,只扫描一层引用,速度极快
  • 通常会搭载一次 Minor GC(可配置),借机减少需要扫描的根

2.2 并发标记(Concurrent Mark)—— 与业务线程并发

  • 从初始标记的根出发,遍历整个老年代对象图,标记所有存活对象
  • 这是整个周期中耗时最长的阶段,但不暂停业务线程
  • 风险:标记过程中业务线程在修改引用关系,会产生标记不一致问题(下文详述)

2.3 重新标记(Remark)—— STW

  • 修正并发标记期间因业务线程运行而导致的标记变动
  • 核心是处理并发标记期间产生/变化的引用:
    • 新晋升到老年代的对象
    • 老年代对象引用关系的变化
  • 借助Write Barrier + 模卡表(Card Table)优化:并发标记期间,所有对老年代对象的引用变更都会把对应 Card 标为 Dirty(增量更新 / Incremental Update),Remark 只需重新扫描 Dirty Card,大幅缩短 STW 时间
  • 可通过-XX:+CMSScavengeBeforeRemark让 Remark 前先做一次 Minor GC,减少扫描新生代的负担

2.4 并发清除(Concurrent Sweep)—— 与业务线程并发

  • 清理标记阶段判定为死亡的对象,回收其空间
  • 由于是标记-清除算法,只回收不搬迁对象,空闲列表(Free List)记录可用空间

停顿时间小结:整个周期中只有 Initial Mark 和 Remark 两个短 STW,且都与堆大小基本无关(只与 GC Roots 数量、Dirty Card 数量相关),这就是 CMS 低停顿的本质。

下面是 CMS 一个完整回收周期内业务线程与 GC 线程时间线的 Mermaid 时序图,横轴为时间,标注了 Initial Mark(STW)、Concurrent Mark、Remark(STW)、Concurrent Sweep 四个阶段,以及各阶段业务线程是否暂停:

GC 线程业务线程GC 线程业务线程阶段一:初始标记(Initial Mark)—— STW业务线程暂停(STW)阶段二:并发标记(Concurrent Mark)—— 与业务线程并发业务线程正常运行(不暂停)阶段三:重新标记(Remark)—— STW业务线程暂停(STW)阶段四:并发清除(Concurrent Sweep)—— 与业务线程并发业务线程正常运行(不暂停)只标记 GC Roots 直接关联的对象遍历老年代对象图,标记存活对象修正并发标记期间的引用变动(重扫 Dirty Card)清理死亡对象,回收空间

说明:图中红色区域(初始标记、重新标记)为 STW 阶段,业务线程暂停;绿色区域(并发标记、并发清除)与业务线程并发执行,业务线程不暂停。整个周期中只有两个短 STW,且都与堆大小基本无关,这就是 CMS 低停顿的本质。


三、Write Barrier:并发的基石

并发标记最大的难点是:标记的同时业务线程在改对象图。经典的三色标记(Tri-color Marking)模型描述了可能出问题的两种场景:

3.1 三色标记模型

三色标记(Tri-color Marking)是并发标记算法的理论基础,它把对象图遍历过程中的对象抽象为三种颜色:

颜色含义
白色尚未被扫描到的对象(初始状态,可能是垃圾)
灰色自身已扫描,但成员引用尚未扫描完(正在处理中)
黑色自身及成员引用均已扫描完成(确认存活)

对象图的遍历过程就是"把白色变灰、灰色变黑"的过程:从 GC Roots 出发,先把根对象标记为灰色,然后逐个扫描灰色对象的成员引用,把引用的白色对象变灰,扫描完自身所有成员后该对象变黑,如此往复直到没有灰色对象为止。

并发标记的安全性要求:扫描结束后不存在"存活对象仍是白色"的情况。如果并发标记期间业务线程修改了对象图,就可能出现"本该存活的对象仍是白色"的漏标(Missed Mark),这是正确性问题,会导致存活对象被错误回收,必须避免。

3.2 两种破坏条件

并发标记期间若业务线程同时做了两个动作,就会产生漏标(浮动垃圾是反向问题,只浪费空间不致命;漏标是正确性问题,致命):

  • 条件一:黑色对象新增了指向白色对象的引用
    • 黑色对象已被视为"扫描完成",若此时它新增指向某个白色对象的引用,该白色对象就不会再被扫描到,最终被误判为垃圾
  • 条件二:灰色对象到该白色对象的引用被删除
    • 灰色对象尚未扫描完,若它到某个白色对象的引用被删除,该白色对象就失去了被扫描的路径,同样会被漏标

漏标的处理方式:要保证不漏标,只需破坏上述任一条件即可,业界有两种经典方案:

  1. 增量更新(Incremental Update)——CMS 采用

    • 针对条件一:当黑色对象插入新指向白色对象的引用时,通过 Write Barrier 记录该黑色对象(存入标记栈),Remark 阶段把它重新变灰、重扫,从而覆盖到新增的白色引用
    • 优点:只记录"新增引用"这一小部分变更,Remark 阶段扫描量小
    • 缺点:需要 STW 的 Remark 阶段来重扫被记录的黑色对象
  2. 原始快照(SATB,Snapshot-At-The-Beginning)——G1 采用

    • 针对条件二:在引用被删除时,通过 Write Barrier 记录被删除的旧引用(即"快照"),保证并发标记开始时存活的对象在标记结束时仍被视为存活
    • 优点:不需要 STW 重扫,标记过程更平滑
    • 缺点:可能保留一些"并发标记期间已死"的对象(浮动垃圾),需要下一轮回收

对比:G1 采用的是原始快照(SATB,Snapshot-At-The-Beginning),在引用被删除时记录旧引用,破坏条件二。这是 CMS 与 G1 在并发标记实现上的核心差异之一,面试高频。

3.2 两种破坏条件

并发标记期间若业务线程同时做了两个动作,就会产生漏标(浮动垃圾是反向问题,只浪费空间不致命;漏标是正确性问题,致命):

  • 条件一:黑色对象新增了指向白色对象的引用
  • 条件二:灰色对象到该白色对象的引用被删除

CMS 采用增量更新(Incremental Update):当黑色对象插入新指向白色对象的引用时,通过 Write Barrier 记录该黑色对象(存入标记栈),Remark 阶段把它重新变灰、重扫。这破坏了条件一。

对比:G1 采用的是原始快照(SATB,Snapshot-At-The-Beginning),在引用被删除时记录旧引用,破坏条件二。这是 CMS 与 G1 在并发标记实现上的核心差异之一,面试高频。

3.3 Card Table 与 Dirty Card

  • 老年代按 512 字节划分为若干 Card,构成全局卡表(Card Table)
  • 新生代 GC 时,通过记录老年代→新生代引用的 Dirty Card 避免全老年代扫描(Remembered Set 思想的简化版)
  • CMS 并发标记阶段复用卡表记录变更,Remark 阶段只扫 Dirty Card
  • 代价:Write Barrier 本身有性能开销,维护卡表也有并发竞争成本

四、CMS 的三大固有问题

4.1 内存碎片(Mark-Sweep 的代价)

  • 标记-清除不移动对象,长期运行后老年代碎片化严重
  • 碎片带来的典型故障:大对象无法分配——总空闲内存足够,但没有一块连续空间,触发 Full GC
  • 缓解手段:-XX:+UseCMSCompactAtFullCollection(JDK 9 前默认开启,Full GC 时整理碎片,但整理过程 STW 且串行);-XX:CMSFullGCsBeforeCompaction=N(N 次 Full GC 后整理一次)

4.2 浮动垃圾(Floating Garbage)

  • 并发清除阶段业务线程仍在运行,此期间产生的垃圾只能等下一轮回收
  • 因此 CMS不能等老年代满了才开始回收,必须提前启动
  • -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly:老年代占用达到 75%(经验值)时触发回收,预留 25% 空间给并发期间的业务分配

下面是 CMS 触发回收时机与老年代占用率关系的 Mermaid 时序图,横轴为时间,纵轴为老年代占用百分比,标注了CMSInitiatingOccupancyFraction=75%的触发线、并发回收窗口,以及 Concurrent Mode Failure 发生时老年代占满的路径:

CMS 回收器老年代业务线程CMS 回收器老年代业务线程老年代占用率随时间上升占用率 < 75%,CMS 不触发占用率达到 75%(CMSInitiatingOccupancyFraction)并发回收窗口:占用率回落,预留 25% 空间给业务分配并发回收期间业务分配过快 / 碎片导致连续空间不足老年代占满,停顿可能达数秒持续分配对象触发并发回收周期并发标记(Concurrent Mark)并发清除(Concurrent Sweep)继续分配对象预留空间不足(Concurrent Mode Failure)回退 Serial Old 单线程整理式 Full GC

说明:图中蓝色区域为正常并发回收窗口——老年代占用达到 75% 触发线时 CMS 提前启动回收,预留 25% 空间给并发期间的业务分配;红色区域为 Concurrent Mode Failure 路径——若并发回收期间业务分配过快或碎片导致连续空间不足,老年代被占满,CMS 回退到 Serial Old 单线程整理式 Full GC,停顿可能达到数秒。

4.3 Concurrent Mode Failure(并发模式失败)

这是 CMS 最著名的故障场景:

  • CMS 并发回收期间,业务线程还在分配内存(并发阶段业务线程与回收线程同时在写老年代)
  • 若预留空间不够分配(尤其是碎片导致连续空间不足),业务线程无法继续分配
  • 此时 JVM 触发Serial Old 单线程整理式 Full GC,停顿可能达到数秒——CMS 的低停顿优势瞬间归零

调优思路(实战):

  1. 调低CMSInitiatingOccupancyFraction(如 70),让 CMS 提前回收
  2. 减少碎片:合理设置CMSFullGCsBeforeCompaction,定期整理
  3. 控制晋升速度:调大新生代、降低对象晋升年龄,避免大促/批处理场景短时间大量对象直接进入老年代
  4. 排查大对象:-XX:PretenureSizeThreshold让超大对象直接进入老年代未必是坏事,但碎片敏感场景要注意
  5. 监控日志:-XX:+PrintGCDetails中出现concurrent mode failure是明确告警信号

另外还有Promotion Failure(晋升失败):Minor GC 时新生代对象要晋升到老年代,但老年代没有足够连续空间(即使是 Minor GC 也会连带触发 Full GC)。


五、常用参数速查

参数作用备注
-XX:+UseConcMarkSweepGC启用 CMS(老年代),新生代默认 ParNewJDK 14 移除
-XX:CMSInitiatingOccupancyFraction=75触发阈值(老年代占比)需配合下一参数才稳定生效
-XX:+UseCMSInitiatingOccupancyOnly严格按上述阈值触发不设则 JVM 会自行启发式调整
-XX:+UseCMSCompactAtFullCollectionFull GC 时整理碎片JDK 9 前默认开
-XX:CMSFullGCsBeforeCompaction=0每 N 次 Full GC 后整理0 表示每次都整理
-XX:+CMSParallelRemarkEnabled并行执行 Remark降低 Remark 停顿
-XX:+CMSScavengeBeforeRemarkRemark 前先 Minor GC减少扫新生代
-XX:+CMSParallelInitialMarkEnabled并行执行初始标记降低初始标记停顿
-XX:ParallelCMSThreadsCMS 并发线程数默认(CPU数+3)/4
-XX:+PrintGCDetails/-Xlog:gc*GC 日志JDK 8 / JDK 9+ 语法

注意ParallelCMSThthreads默认值的含义:回收线程会占用 1/4 的 CPU 资源。如果应用本身 CPU 紧张(比如量化交易这类低延迟场景),并发阶段业务吞吐会被明显拉低——低停顿是用 CPU 换来的。


六、CMS vs G1 vs ZGC

维度CMSG1ZGC/Shenandoah
算法标记-清除标记-整理(Region 整体复制)染色指针/读屏障 + 转发指针
堆布局物理分代(连续)逻辑分代(Region 网格)Region,不分代(ZGC 初期)
碎片有无无
并发标记增量更新SATB各有实现
停顿量级10~200ms10~200ms(可预测模型)<1ms / 亚毫秒
停顿与堆大小基本无关基本无关完全无关
适用堆≤8G4G~32G百 GB~TB 级
状态已移除(JDK 14)JDK 9+ 默认JDK 15+ 生产可用

CMS 的历史地位在于证明了并发回收这条路走得通;G1 用 Region 化解决了碎片问题;ZGC/Shenandoah 用着色指针/读屏障把并发推进到了**并发整理(对象移动也可与业务线程并发)**的时代。


七、思考

Q1:CMS 为什么不直接用标记-整理?
移动对象必须修正所有指向该对象的引用,这个操作无法与业务线程并发安全地完成(会看到"对象正在被搬运"的中间状态),只能 STW。为了保住低停顿,CMS 拆中选择了标记-清除,用碎片问题换取停顿时间。

Q2:CMS 什么时候触发 Full GC?
三种情况:① 老年代空间不足以支撑并发回收(Concurrent Mode Failure);② 晋升失败(Promotion Failed);③ 显式调用System.gc()(可用-XX:+DisableExplicitGC禁用,但注意堆外内存 DirectByteBuffer 依赖 System.gc() 分配,NIO 重度应用建议改用-XX:+ExplicitGCInvokesConcurrent)。

Q3:CMS 新生代为什么必须配 ParNew?
CMS 需要特定的 Write Barrier 逻辑配合其并发标记,Serial/Parallel Scavenge 无法与之协同。JDK 8 时代 CMS + ParNew 是标准组合。

Q4:CMSInitiatingOccupancyFraction 设多少合适?
经验值 70~80。设太高容易 Concurrent Mode Failure,太低则频繁触发并发回收、浪费 CPU。对碎片敏感或晋升频繁的应用应偏低。配合-XX:+UseCMSInitiatingOccupancyOnly保证行为可预测。

Q5:生产上怎么发现 CMS 的碎片问题?
GC 日志中反复出现concurrent mode failure且伴随 Serial Old 的长停顿;老年代使用率呈"锯齿但底部逐渐抬高"形态;jstat -gcutil观察老年代 Full GC 后回落到的水位一次比一次高。


八、总结

CMS 的历史贡献是确立了并发回收的设计范式:用并发标记 + 短停顿修正的模式把停顿从"秒级、与堆大小正相关"降到"毫秒级、基本与堆无关"。它的两大先天缺陷——内存碎片和浮动垃圾带来的空间预留压力——根源于标记-清除算法,无法根治,最终被 Region 化、支持并发整理的 G1 取代。

对今天仍在维护 JDK 8 老系统的工程师,CMS 调优仍然是实战课题:控制碎片、监控 Concurrent Mode Failure、合理设置触发阈值;而对新系统,JDK 17/21 + G1(或 ZGC)已是更优选择。理解 CMS 的演进逻辑,也就理解了现代低延迟回收器的设计动机。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 8:35:02

把Excel当成发薪系统,到底算漏了哪笔账?

很多做劳务派遣或人力资源外包的朋友&#xff0c;在刚起步甚至团队做到上百人规模时&#xff0c;都有过一种极其坚定的“工具自信”&#xff1a;“不就是招人、排班、算工资、打款吗&#xff1f;我一个十几张工作表的Excel大工程&#xff0c;公式拉满&#xff0c;VLOOKUP用得行…

作者头像 李华
网站建设 2026/10/1 8:34:17

编译原理课后习题答案使用指南:从词法分析到Flex+Bison实践

简介&#xff1a;《编译原理及实践》是编译技术入门到进阶的常备教材&#xff0c;这份PDF是配套课后习题答案&#xff0c;适合计算机专业学生、考研复习者以及自学编译器原理的开发者。资源围绕词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成与错误处理等…

作者头像 李华
网站建设 2026/10/1 8:34:14

FreeMarker实战指南:从模板语法到SpringBoot集成与代码生成

说实话&#xff0c;看到热搜词里的“前端开发者学习后端Java知识计划”“springmv freemarker 转成springboot项目”&#xff0c;我第一反应是&#xff1a;越来越多的人正在从前后端分离体系倒回去接触老牌模板引擎。不少刚入行的后端同学觉得FreeMarker是过时技术&#xff0c;…

作者头像 李华
网站建设 2026/10/1 8:33:19

初学前端的第一篇笔记

一、准备知识 1.两位先驱&#xff1a;图灵与冯诺依曼 2.计算机由硬件和软件组成&#xff0c;软件分为系统软件与应用软件 3.应用软件又分为两大类&#xff1a; C/S架构&#xff0c;特点&#xff1a;需要安装、偶尔更新、不跨平台、开发更具针对性。 B/S架构&#xff0c;特点&am…

作者头像 李华