垃圾回收器汇总
了解下 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 垃圾回收器需要卡表,主要是为了优化跨代引用扫描效率,解决相关效率问题:
- 跨代引用引发性能瓶颈:在 YGC(年轻代垃圾回收)时,老年代对象可能引用年轻代对象。要是直接扫描整个老年代来找跨代引用,时间和资源消耗太大,无法接受。
- 并发标记阶段记录引用变化:CMS 并发标记阶段,老年代引用变化会通过写屏障标记到卡表。这么做的核心目的,是在重新标记阶段能高效修正并发期间遗漏的存活对象标记。此时,卡表不光用于跨代引用跟踪,还处理老年代内部引用变更的记录。
- 卡表的关键作用:卡表把老年代划分为固定大小的卡片(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 漏标。
✅ 解决过程
- 写屏障拦截引用变更:
- 用户线程新增引用时,触发写屏障,记录引用变化。
- 将 A 从黑色重新标记为灰色,加入灰色队列,防止漏标。
- 重新标记阶段修复漏标:
- 在 STW 阶段,GC 对 A 及其引用的 B 进行重新扫描。
- 将 B 标记为灰色,确保存活对象不被漏标回收。
✅ 4️⃣ 漏标问题的优化与调优
- 增量更新 + 写屏障
- 通过写屏障在引用变更时及时记录,防止漏标。
- 增量更新确保新引用关系被重新扫描,防止遗漏。
- 重新标记阶段
- 在 STW 下重新扫描引用链,确保标记准确。
- 配合 预清理 和 Young GC 减少老年代压力。
- 参数调优
-XX:CMSInitiatingOccupancyFraction=70 # 提前触发 GC,避免漏标
-XX:+UseCMSCompactAtFullCollection # Full GC 时压缩内存,减少碎片
-XX:+CMSScavengeBeforeRemark # 重新标记前执行年轻代 GC
🎯 ✅ 总结
CMS 通过以下机制解决漏标问题:
- 写屏障 + 增量更新
- 在引用变化时记录变更,防止漏标。
- 将黑色对象重新标记为灰色,重新扫描引用关系。
- 重新标记阶段
- 在 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 通过以下机制缓解多标(浮动垃圾)问题:
- 浮动垃圾容忍机制
- CMS 设计上允许浮动垃圾存在,通过预留空间缓解。
- 预清理 + 重新标记阶段
- 通过二次标记修复部分多标对象。
- Full GC 压缩整理
- 清理浮动垃圾并压缩内存碎片。
- Young GC 提前清理
- 在老年代 GC 前先清理年轻代,减少浮动垃圾堆积。
✅ 类比总结 - 浮动垃圾容忍 → 保留部分“废石头”作为预留空间。
- 预清理 + Remark → 中途检查,减少遗漏。
- Full GC → 定期大扫除,清理浮动垃圾。
- Young GC → 提前清理年轻代,减少浮动垃圾堆积。