ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JVM CMS 垃圾回收器深度解析

JVM CMS 垃圾回收器深度解析 JVM CMS 垃圾回收器深度解析CMSConcurrent Mark Sweep并发标记清除是 JVM 历史上第一款真正意义上的低停顿老年代垃圾回收器从 JDK 1.4 引入JDK 9 被标记为 DeprecatedJDK 14 正式移除。虽然它已退出历史舞台但其设计思想并发回收、分阶段标记直接影响了后来的 G1、ZGC、Shenandoah同时也是高级 Java 面试的高频考点。一、CMS 的设计目标在 CMS 出现之前老年代回收主要依靠 Serial Old单线程、STW和 Parallel Old多线程、STW。这两者的停顿时间与堆大小正相关——堆越大停顿越长。CMS 的核心目标是将大部分回收工作放到与用户线程并发执行的阶段把 STW 时间压缩到毫秒级且停顿时间不随堆大小线性增长。这个目标决定了它的两个基本策略并发Concurrent标记和清除阶段与业务线程同时运行标记-清除Mark-Sweep不采用复制/整理算法避免移动对象带来的长时间停顿二、回收的四个阶段CMS 的一个完整回收周期分为四个阶段其中两个 STW、两个并发下面是 CMS 完整回收周期的 Mermaid 流程图标注了各阶段的 STW 情况以及并发失败Concurrent Mode Failure时回退到 Serial Old 的路径是否Concurrent Mode FailureCMS 回收周期开始初始标记Initial MarkSTW并发标记Concurrent Mark与业务线程并发并发期间预留空间是否足够重新标记RemarkSTW回退 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 UpdateRemark 只需重新扫描 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 MarkSTW、Concurrent Mark、RemarkSTW、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 两种破坏条件并发标记期间若业务线程同时做了两个动作就会产生漏标浮动垃圾是反向问题只浪费空间不致命漏标是正确性问题致命条件一黑色对象新增了指向白色对象的引用黑色对象已被视为扫描完成若此时它新增指向某个白色对象的引用该白色对象就不会再被扫描到最终被误判为垃圾条件二灰色对象到该白色对象的引用被删除灰色对象尚未扫描完若它到某个白色对象的引用被删除该白色对象就失去了被扫描的路径同样会被漏标漏标的处理方式要保证不漏标只需破坏上述任一条件即可业界有两种经典方案增量更新Incremental Update——CMS 采用针对条件一当黑色对象插入新指向白色对象的引用时通过 Write Barrier 记录该黑色对象存入标记栈Remark 阶段把它重新变灰、重扫从而覆盖到新增的白色引用优点只记录新增引用这一小部分变更Remark 阶段扫描量小缺点需要 STW 的 Remark 阶段来重扫被记录的黑色对象原始快照SATBSnapshot-At-The-Beginning——G1 采用针对条件二在引用被删除时通过 Write Barrier 记录被删除的旧引用即快照保证并发标记开始时存活的对象在标记结束时仍被视为存活优点不需要 STW 重扫标记过程更平滑缺点可能保留一些并发标记期间已死的对象浮动垃圾需要下一轮回收对比G1 采用的是原始快照SATBSnapshot-At-The-Beginning在引用被删除时记录旧引用破坏条件二。这是 CMS 与 G1 在并发标记实现上的核心差异之一面试高频。3.2 两种破坏条件并发标记期间若业务线程同时做了两个动作就会产生漏标浮动垃圾是反向问题只浪费空间不致命漏标是正确性问题致命条件一黑色对象新增了指向白色对象的引用条件二灰色对象到该白色对象的引用被删除CMS 采用增量更新Incremental Update当黑色对象插入新指向白色对象的引用时通过 Write Barrier 记录该黑色对象存入标记栈Remark 阶段把它重新变灰、重扫。这破坏了条件一。对比G1 采用的是原始快照SATBSnapshot-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:UseCMSCompactAtFullCollectionJDK 9 前默认开启Full GC 时整理碎片但整理过程 STW 且串行-XX:CMSFullGCsBeforeCompactionNN 次 Full GC 后整理一次4.2 浮动垃圾Floating Garbage并发清除阶段业务线程仍在运行此期间产生的垃圾只能等下一轮回收因此 CMS不能等老年代满了才开始回收必须提前启动-XX:CMSInitiatingOccupancyFraction75 -XX:UseCMSInitiatingOccupancyOnly老年代占用达到 75%经验值时触发回收预留 25% 空间给并发期间的业务分配下面是 CMS 触发回收时机与老年代占用率关系的 Mermaid 时序图横轴为时间纵轴为老年代占用百分比标注了CMSInitiatingOccupancyFraction75%的触发线、并发回收窗口以及 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 的低停顿优势瞬间归零调优思路实战调低CMSInitiatingOccupancyFraction如 70让 CMS 提前回收减少碎片合理设置CMSFullGCsBeforeCompaction定期整理控制晋升速度调大新生代、降低对象晋升年龄避免大促/批处理场景短时间大量对象直接进入老年代排查大对象-XX:PretenureSizeThreshold让超大对象直接进入老年代未必是坏事但碎片敏感场景要注意监控日志-XX:PrintGCDetails中出现concurrent mode failure是明确告警信号另外还有Promotion Failure晋升失败Minor GC 时新生代对象要晋升到老年代但老年代没有足够连续空间即使是 Minor GC 也会连带触发 Full GC。五、常用参数速查参数作用备注-XX:UseConcMarkSweepGC启用 CMS老年代新生代默认 ParNewJDK 14 移除-XX:CMSInitiatingOccupancyFraction75触发阈值老年代占比需配合下一参数才稳定生效-XX:UseCMSInitiatingOccupancyOnly严格按上述阈值触发不设则 JVM 会自行启发式调整-XX:UseCMSCompactAtFullCollectionFull GC 时整理碎片JDK 9 前默认开-XX:CMSFullGCsBeforeCompaction0每 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 14JDK 9 默认JDK 15 生产可用CMS 的历史地位在于证明了并发回收这条路走得通G1 用 Region 化解决了碎片问题ZGC/Shenandoah 用着色指针/读屏障把并发推进到了**并发整理对象移动也可与业务线程并发**的时代。七、思考Q1CMS 为什么不直接用标记-整理移动对象必须修正所有指向该对象的引用这个操作无法与业务线程并发安全地完成会看到对象正在被搬运的中间状态只能 STW。为了保住低停顿CMS 拆中选择了标记-清除用碎片问题换取停顿时间。Q2CMS 什么时候触发 Full GC三种情况① 老年代空间不足以支撑并发回收Concurrent Mode Failure② 晋升失败Promotion Failed③ 显式调用System.gc()可用-XX:DisableExplicitGC禁用但注意堆外内存 DirectByteBuffer 依赖 System.gc() 分配NIO 重度应用建议改用-XX:ExplicitGCInvokesConcurrent。Q3CMS 新生代为什么必须配 ParNewCMS 需要特定的 Write Barrier 逻辑配合其并发标记Serial/Parallel Scavenge 无法与之协同。JDK 8 时代 CMS ParNew 是标准组合。Q4CMSInitiatingOccupancyFraction 设多少合适经验值 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 的演进逻辑也就理解了现代低延迟回收器的设计动机。
返回列表