图解CMS原理是什么?漏标+多标+浮动垃圾 如何解决?

垃圾回收器汇总

了解下 HotSpot虚拟机中 9款 垃圾回收器 的发布时间及其对应的 JDK版本,如下图:
在这里插入图片描述
就目前来说,JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC,下面我们来看看这两大类的垃圾收集器。
说明:分代垃圾收集器中,新生代有 Serial、ParNew、Parallel Scavenge,老年代包括 CMS、MSC、Parallel old,收集器之间的连线说明两者可以搭配使用。
在这里插入图片描述

CMS底层原理

垃圾回收器汇总

四阶段工作流程

  • 初始标记(Initial Mark)‌:STW阶段,标记GC Roots直接关联的对象;
  • 并发标记(Concurrent Mark)‌:与用户线程并发执行,遍历老年代对象引用链;
  • 重新标记(Remark)‌:STW阶段,修正并发标记期间变动的引用关系(通过增量更新或原始快照算法)‌;
  • 并发清除(Concurrent Sweep)‌:删除无引用对象,回收内存空间。’

关键技术机制

  • 卡表(Card Table)‌:将老年代划分为512字节的卡片,记录跨代引用,避免YGC时扫描整个老年代;
  • 增量并发:通过交替执行GC线程与用户线程减少STW时间,但可能引发并发失败(Concurrent Mode Failure)。

为什么有的文章 cms 是 4个阶段, 有的 文章 cms 是 6个阶段 ?
CMS垃圾回收器的阶段划分存在差异,主要是因为不同的资料对阶段的划分标准和详细程度有所不同。有的资料将CMS的执行过程简化为四个主要阶段,而另一些资料则更详细地描述了六个阶段,包括预清理和可中断的预清理阶段。

区分卡表(Card Table)与记忆集(Remembered Set),前者用于跨代引用,后者用于分代收集器的跨区域引用。

CMS的核心机制是什么?

(1) 并发标记与清理:通过多线程与用户线程并发执行标记和清理操作,仅初始标记和重新标记阶段需短暂STW
(2) 三色标记法:基于黑(已标记且存活)、灰(标记中)、白(未标记或垃圾)的对象状态跟踪,结合写屏障(Write Barrier)记录并发阶段的对象引用变化,防止漏标
(3) 内存碎片处理:标记- 清除算法不压缩内存 ,长期运行后可能产生碎片,依赖Full GC(Serial Old)或参数触发碎片整理**

CMS的特点

  • 并发收集:GC 线程与用户线程并发执行
  • 低停顿:追求最短回收停顿时间
  • 标记-清除算法:会产生内存碎片
  • 分代收集:与 ParNew 配合使用(年轻代用 ParNew,老年代用 CMS)

CMS 几个基本数据结构

结构1:卡表

在这里插入图片描述

CMS 垃圾回收器依赖卡表的原因

CMS 垃圾回收器需要卡表,主要是为了优化跨代引用扫描效率,解决相关效率问题:

  1. 跨代引用引发性能瓶颈:在 YGC(年轻代垃圾回收)时,老年代对象可能引用年轻代对象。要是直接扫描整个老年代来找跨代引用,时间和资源消耗太大,无法接受。
  2. 并发标记阶段记录引用变化:CMS 并发标记阶段,老年代引用变化会通过写屏障标记到卡表。这么做的核心目的,是在重新标记阶段能高效修正并发期间遗漏的存活对象标记。此时,卡表不光用于跨代引用跟踪,还处理老年代内部引用变更的记录。
  3. 卡表的关键作用:卡表把老年代划分为固定大小的卡片(Card,一般 512 字节),并记录卡片是否被修改(脏卡标记)。YGC 时,只需扫描标记为脏的卡片,不用全量扫描老年代,大大缩小了扫描范围,在 YGC 中发挥重要作用。

什么是卡表

对于分代垃圾回收器,势必存在一个跨代引用的问题,而卡表就是最常用的一种跨代引用 记录结构 ,它是一个字节数组,用于记录堆内存的映射关系,下面是 HotSpot虚拟机默认的卡表标记逻辑:
[图片]
在 CMS 垃圾收集器里,卡表主要用来记录老年代对年轻代的引用,以此加速年轻代垃圾收集(YGC)进程。YGC 时,卡表能快速锁定可能引用年轻代对象的老年代对象,不用扫描整个老年代,提升了效率。

但在 Full GC 场景下,卡表作用有限。Full GC 会扫描整个堆内存,全面处理所有对象引用,无需卡表定位跨代引用。不过,Full GC 时仍会维护卡表,对其清理,保证内容契合堆内存实际情况,为后续 YGC 正常运作做准备。虽说这会带来些开销,但相比 Full GC 的全局扫描与对象回收,开销较小。

总体而言,Full GC 中卡表贡献小,它主要助力 YGC 加速跨代引用处理 。

结构2:写屏障

在 HotSpot 虚拟机 中,写屏障(Write Barrier) 是一种环绕切面(AOP)机制,在对象引用赋值的前后插入额外动作(如更新卡表)。写屏障分为:

  • 前置写屏障(Pre-Write-Barrier): 赋值前记录旧引用。
  • 后置写屏障(Post-Write-Barrier): 赋值后记录新引用。
    📌 注意: 写屏障与多线程的内存屏障不同,前者用于 GC 跟踪引用变化,后者防止指令重排。

🔥 写屏障的作用:引用关系的“监督员”
在垃圾回收时,写屏障就像“引用关系监督员”,记录对象引用的变化,防止回收器遗漏存活对象。
✅ 类比场景:

  • 内存中的对象像图书馆里的书籍,对象引用关系类似书与书的关联。
  • 写屏障就像管理员,记录书籍的引用变化,确保不会误删仍在使用的书。

🚀 解决三色标记法的漏标问题
在三色标记法中:

  • ⚫️ 黑色对象: 标记完成,不会被回收。
  • ⚙️ 灰色对象: 正在标记中。
  • ⚪️ 白色对象: 未标记,可能被回收。
    漏标问题:
  • 标记阶段若引用关系改变,可能导致某些白色对象未被标记,被误回收。
    ✅ 写屏障的作用:
  • 当引用关系变化时,写屏障会检查新引用对象是否为灰色或黑色。
  • 若是,则将相关的白色对象标记为灰色,防止漏标。
    ✅ 类比场景:
  • 垃圾回收像寻宝游戏,白色对象是“待回收的宝藏”。
  • 写屏障就像哨兵,发现引用变更时,立刻标记宝藏,防止遗漏。

🎯 总结

  • 写屏障 = 引用变动的监督员,时刻记录引用变化,防止漏标。
  • 在三色标记法中,确保存活对象不会因引用关系改变而被误回收。
  • 类比: 垃圾回收中的“哨兵”,保护存活对象不被误删。 🚀

CMS的垃圾回收的触发时机

1️⃣ 老年代内存占用过高

  • 老年代空间使用率超过 -XX:CMSInitiatingOccupancyFraction 参数(默认 92%)时,触发初始标记。
  • 自定义阈值时,需启用 -XX:+UseCMSInitiatingOccupancyOnly,防止 JVM 动态调整。

2️⃣ 元数据区(Metaspace)空间不足

  • 当元数据区内存不足(如加载大量类)时,自动触发 CMS 初始标记。

3️⃣ 年轻代晋升失败

  • 年轻代 GC 后,存活对象晋升到老年代。
  • 若老年代空间不足,则触发初始标记。
  • 参数优化:-XX:+CMSScavengeBeforeRemark 可在初始标记前先执行年轻代 GC,减少晋升失败。

4️⃣ 显式调用 GC

  • 手动调用 System.gc() 时,若启用 -XX:+ExplicitGCInvokesConcurrent,则直接启动 CMS 初始标记。

CMS优缺点分析

🚀 ✅ 核心优点
1️⃣ 低延迟设计

  • 并发标记与清除:CMS 在 Concurrent Mark(并发标记) 和 Concurrent Sweep(并发清除) 阶段与用户线程并行工作,有效减少 STW(Stop-The-World) 停顿,适合实时系统等低延迟场景。
  • 可控停顿:通过 -XX:MaxGCPauseMillis 参数可设置最大停顿时间,保障用户体验。

2️⃣ 分代收集优化

  • 老年代 + 年轻代:CMS 针对老年代设计,配合年轻代 ParNew 收集器,实现分代回收,减少整体 GC 压力。

⚠️ ❌ 核心缺点
1️⃣ 内存碎片问题

  • 标记-清除算法缺陷:CMS 采用标记-清除算法,不压缩内存,易产生大量 内存碎片,影响分配效率。
  • 频繁 Full GC 风险:碎片过多时,容易触发 Full GC。可启用 -XX:+UseCMSCompactAtFullCollection 参数在 Full GC 时压缩内存,但代价是更长的 STW。

2️⃣ CPU 资源敏感

  • 并发阶段占用资源:CMS 并发阶段会占用约 25%~30% 的 CPU 资源,默认线程数公式:(CPU核数 + 3) / 4。
  • 高并发场景瓶颈:在高并发或低配环境下,CMS 会影响应用吞吐量。

3️⃣ 浮动垃圾处理不足

  • 浮动垃圾:并发标记期间,用户线程可能持续创建新对象(浮动垃圾),CMS 无法及时回收,需等下次 GC 处理。
  • Concurrent Mode Failure(CMF):若老年代预留空间不足(默认阈值 -XX:CMSInitiatingOccupancyFraction=92%),会触发 Full GC,退化为单线程的 Serial Old 收集器,引发长时间 STW。

4️⃣ 维护与兼容性问题

  • 已淘汰技术:CMS 从 JDK 9 开始被标记为废弃,JDK 14 中彻底移除,不再受官方支持,仅适用于老旧系统维护。
    🎯 总结:CMS 的适用性
    ✅ 适合:低延迟场景(如交易系统、游戏服务器)。
    ❌ 不适合:内存敏感、高并发场景,需谨防浮动垃圾与内存碎片。
    🚫 JDK 14+ 环境建议使用 G1 或 ZGC,CMS 不再受官方支持。

Concurrent Mode Failure 是什么?

⚠️ ❌ 什么是 CMF?
Concurrent Mode Failure 是 CMS 垃圾回收器在并发阶段内存不足时触发的关键问题。

  • 本质原因:CMS 并发回收期间,老年代预留空间不足,导致无法分配新对象,从而触发 Full GC(退化为单线程 Serial Old 收集器),造成长时间 STW 停顿。

🔥 CMF 的触发场景
1️⃣ 老年代预留空间不足

  • 触发机制:CMS 并发回收时,需预留部分内存给用户线程分配新对象。若预留空间不足,分配失败,触发 CMF。
  • 默认阈值:-XX:CMSInitiatingOccupancyFraction=92%(老年代占用率达 92% 时触发 GC)。
  • 典型表现:并发回收阶段,用户线程持续分配对象,导致预留空间被耗尽。

2️⃣ 晋升失败(Promotion Failed)

  • 场景:Young GC 后,新生代对象晋升至老年代,但老年代没有足够连续空间存放,即使总空闲空间充足。
  • 原因:
    • 内存碎片严重,无法容纳大对象。
    • 晋升速率超过 CMS 回收速度,导致空间不足。

3️⃣ 大对象直接分配失败

  • 情况:应用直接在老年代分配大对象(如 -XX:PretenureSizeThreshold 设置大对象直接进入老年代),但老年代没有足够连续空间时,触发 CMF。

4️⃣ 回收速度滞后

  • 原因:CMS 并发回收速度慢于对象分配速度,预留空间被迅速消耗。
  • 表现:并发标记时间过长,导致回收跟不上分配节奏。

🔧 CMF 优化策略

✅ 1️⃣ 降低触发阈值,预留更多空间

  • 参数调整:
    -XX:CMSInitiatingOccupancyFraction=70 # 降低阈值(如70%),预留更多空间给用户线程
    -XX:+UseCMSInitiatingOccupancyOnly # 固定阈值,防止 JVM 动态调整
  • 作用:提高 CMS 回收提前量,避免老年代空间被过度占用。

✅ 2️⃣ 减少内存碎片

  • 参数配置:
    -XX:+UseCMSCompactAtFullCollection # Full GC 时压缩碎片,减少内存浪费
    -XX:CMSFullGCsBeforeCompaction=5 # 每5次 Full GC 压缩一次
  • 作用:清理碎片,提升晋升成功率,降低 CMF 风险。

✅ 3️⃣ 优化 GC 调度

  • 参数配置:
    -XX:+CMSScavengeBeforeRemark # 在重新标记前触发 Young GC
  • 作用:减少跨代引用,减轻老年代压力,降低 CMF 发生概率。

✅ 4️⃣ 监控与扩容

  • 监控命令:
    jstat -gcutil 1000 # 实时监控老年代占用率与 GC 频率
  • 扩容措施:
    • 增大堆内存或老年代比例。
    • 调整 -Xms 和 -Xmx 参数,确保老年代空间充足。

🎯 总结:CMF 优化思路
✅ 预留更多空间: 降低触发阈值,提前回收。
✅ 压缩碎片: 减少内存浪费,防止晋升失败。
✅ 优化 GC 调度: 使用 -XX:+CMSScavengeBeforeRemark 提前释放年轻代空间。
✅ 监控与扩容: 实时监控内存占用率,必要时扩容。
👉 推荐方案:
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-XX:+CMSScavengeBeforeRemark
✅ 这样可有效减少 CMF 发生概率,保证 GC 效率与应用性能。

三色标记的漏标、多标问题

🎯 1️⃣ 三色标记算法的基本概念

在垃圾回收过程中,三色标记算法将对象分为三类:

  • 白色(未访问):尚未被标记的对象,表示“未知区域”,可能是垃圾。
  • 灰色(已访问,引用未遍历完):已标记但引用的对象未完全处理,表示“待检查区域”。
  • 黑色(已访问且引用遍历完):已标记且引用的对象也全部标记完毕,表示“已处理区域”。

⚠️ 2️⃣ 漏标与多标问题

在 并发标记阶段,GC 与用户线程同时运行,可能导致以下两种问题:
✅ 漏标(漏掉存活对象)
暂时无法在飞书文档外展示此内容

  • 定义:本应存活的对象未被标记,错误地当作垃圾对象回收。
  • 触发场景:
    • 并发标记期间,用户线程将白色对象变为可达(引用链被建立),但 GC 已经遍历过该引用链,导致该白色对象未被标记为存活。
  • 结果:存活对象被误回收。
  • 类比:就像在挖宝藏时遗漏了“真正的宝藏”。
    在这里插入图片描述

✅ 多标(多标记垃圾对象)
暂时无法在飞书文档外展示此内容

  • 定义:本应回收的垃圾对象被多余标记为存活对象,无法回收。
  • 触发场景:
    • 并发标记阶段,用户线程将黑色对象的引用删除,导致该对象实际上已不可达,但由于其已标记为黑色,GC 仍将其当作存活对象。
  • 结果:垃圾对象未被回收,形成 浮动垃圾。
  • 类比:相当于把“废弃的宝藏”当作有价值的东西留了下来。

在这里插入图片描述

🔧 3️⃣ 漏标与多标的影响

  • 漏标:存活对象被误回收,可能导致程序异常甚至崩溃。
  • 多标:垃圾对象未能回收,形成 浮动垃圾,增加内存占用,降低 GC 效率。

大厂面试:CMS如何 解决 漏标 ?

CMS 垃圾回收器采用以下两种机制解决漏标:


🔧 (1)写屏障 + 增量更新

核心思想:在并发标记阶段,使用 写屏障(Write Barrier)记录对象引用变化,确保引用关系变更不会被遗漏。
✅ 写屏障工作机制

  • 作用:在引用关系发生变化时,拦截引用变更,记录变更信息。
  • 实现方式:
    • 当 黑色对象增加对白色对象的引用时,CMS 使用写屏障将该黑色对象重新标记为灰色。
    • 将该对象加入 灰色队列,在重新标记阶段重新扫描它的引用,确保引用变化被正确标记。
  • 类比:写屏障就像“监督员”,在引用变化时记录异常,确保 GC 不会遗漏存活对象。
    ✅ 增量更新(Incremental Update)
  • 原理:
    • 当黑色对象新增对白色对象的引用时,将黑色对象重新标记为灰色。
    • 在后续标记阶段重新扫描该对象的引用关系,确保引用变化被正确追踪。
  • 缺点:
    • 增量更新可能会导致重新扫描黑色对象的所有引用,增加开销,影响 GC 效率。
  • 类比:增量更新就像在检查宝藏时,如果发现有新加入的宝藏,就重新标记一遍,防止遗漏。
    ✅ 代码参数
    bash
    复制编辑
    -XX:+CMSIncrementalMode # 启用增量更新模式,适合多 CPU 并行处理
    -XX:+CMSIncrementalUpdate # 增量更新方式,防止漏标

🔥 (2)重新标记阶段(Remark Phase)

核心作用:重新标记阶段是 解决漏标问题的关键步骤,在该阶段,CMS 会暂停用户线程,对可能被漏标的对象进行二次检查和校正。
✅ 工作机制

  • STW(Stop-The-World):
    • CMS 会暂停所有用户线程,确保标记过程的完整性和准确性。
  • 重新扫描可能漏标的对象:
    • CMS 检查并发标记阶段通过写屏障记录下的引用变化。
    • 对这些引用关系进行重新遍历,确保所有存活对象被正确标记。
  • 校正标记结果:
    • 将误标为白色的存活对象重新标记为灰色,避免被错误回收。
    • 这种校正机制保证了标记阶段的准确性。
      ✅ 类比:
      重新标记阶段就像在丢弃废品前,再次仔细检查一遍,防止将有价值的宝藏误当作垃圾扔掉。
      ✅ 代码参数
      -XX:+CMSScavengeBeforeRemark # 在重新标记前触发 Young GC,减少老年代压力
      -XX:CMSMaxAbortablePrecleanTime=5000 # 控制 Remark 阶段的最大预清理时间

🚀 3️⃣ 示例:CMS 如何修复漏标

假设以下情况:

  • 黑色对象 A 引用了白色对象 B。
  • 标记阶段,用户线程新增了 A → B 的引用关系。
  • CMS 未标记 B,导致 B 漏标。
    ✅ 解决过程
  1. 写屏障拦截引用变更:
  • 用户线程新增引用时,触发写屏障,记录引用变化。
  • 将 A 从黑色重新标记为灰色,加入灰色队列,防止漏标。
  1. 重新标记阶段修复漏标:
  • 在 STW 阶段,GC 对 A 及其引用的 B 进行重新扫描。
  • 将 B 标记为灰色,确保存活对象不被漏标回收。

✅ 4️⃣ 漏标问题的优化与调优

  • 增量更新 + 写屏障
    • 通过写屏障在引用变更时及时记录,防止漏标。
    • 增量更新确保新引用关系被重新扫描,防止遗漏。
  • 重新标记阶段
    • 在 STW 下重新扫描引用链,确保标记准确。
    • 配合 预清理 和 Young GC 减少老年代压力。
  • 参数调优
    -XX:CMSInitiatingOccupancyFraction=70 # 提前触发 GC,避免漏标
    -XX:+UseCMSCompactAtFullCollection # Full GC 时压缩内存,减少碎片
    -XX:+CMSScavengeBeforeRemark # 重新标记前执行年轻代 GC

🎯 ✅ 总结

CMS 通过以下机制解决漏标问题:

  1. 写屏障 + 增量更新
  • 在引用变化时记录变更,防止漏标。
  • 将黑色对象重新标记为灰色,重新扫描引用关系。
  1. 重新标记阶段
  • 在 STW 阶段重新扫描写屏障记录的引用变化,修复漏标。
  • 确保所有存活对象被正确标记,防止被错误回收。
    ✅ 类比总结
  • 写屏障 → “宝藏监督员”,随时记录引用变化。
  • 增量更新 → “重新挖掘”,将黑色对象变灰,重新检查。
  • 重新标记阶段 → “最终复查”,丢弃前再次检查,防止遗漏宝藏。
    ✅ 面试重点
  • CMS 漏标的本质原因:GC 与用户线程并发执行,导致引用变更未被记录。
  • CMS 漏标的解决方案:写屏障 + 增量更新 + 重新标记阶段。
  • 参数调优:配合 Young GC、STW 重新标记,确保标记完整性。

大厂面试:CMS如何 解决 多标 ?(浮动垃圾)

🎯 1️⃣ 多标问题的本质

多标问题是指在 GC 标记阶段,本应回收的垃圾对象被错误标记为存活对象,导致没有被及时回收,形成 浮动垃圾(Floating Garbage)。
💡 多标的成因

  • GC 与用户线程并发执行:
    • 在 CMS 并发标记阶段,用户线程可能删除某个引用关系,使对象变成垃圾,但由于 CMS 不会重新检查已标记为黑色的对象,导致该对象被错误保留。
  • 浮动垃圾:
    • 被多标的对象实际上已经不可达,但由于 GC 标记阶段已经结束,它们仍然被错误标记为存活对象,无法在本轮 GC 被回收。
  • 后果:
    • 浮动垃圾会占用内存空间,影响后续 GC 效率和应用性能。
    • CMS 需要在下一次 GC 中才能回收这些对象,导致内存回收滞后。
      ✅ 类比:
      在挖宝藏时,本应丢弃的“废石头”被误认为“宝藏”保留了下来,导致占用了仓库空间。

例如下图中,假设我们现在遍历到了节点 E,此时应用执行了 objD.fieldE = null;。
那么此刻之后,对象 E、F、G 应该是被回收的。
但因为节点 E 已经是灰色的,那么 E、F、G 节点都会被标记为存活的黑色状态,并不会被回收。

在这里插入图片描述
多标问题会导致内存产生浮动垃圾,但好在其可以再下次 GC 的时候被回收,因此问题还不算很严重。浮动垃圾是由于在并发标记阶段,某些对象被标记为存活,但在标记完成后,这些对象实际上已经不可达。

由于CMS的并发特性,这些对象无法在本次GC中被回收,只能等待下一次GC。CMS通过以下方式处理浮动垃圾:

CMS 通过以下机制缓解多标(浮动垃圾)问题:

  1. 浮动垃圾容忍机制
  • CMS 设计上允许浮动垃圾存在,通过预留空间缓解。
  1. 预清理 + 重新标记阶段
  • 通过二次标记修复部分多标对象。
  1. Full GC 压缩整理
  • 清理浮动垃圾并压缩内存碎片。
  1. Young GC 提前清理
  • 在老年代 GC 前先清理年轻代,减少浮动垃圾堆积。
    ✅ 类比总结
  • 浮动垃圾容忍 → 保留部分“废石头”作为预留空间。
  • 预清理 + Remark → 中途检查,减少遗漏。
  • Full GC → 定期大扫除,清理浮动垃圾。
  • Young GC → 提前清理年轻代,减少浮动垃圾堆积。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值