面试记录 2026-09-21
题目:什么是三色标记算法?
难度:low
标签:jvm/gc, general/knowledge, difficulty/low
评分:⭐☆☆☆☆(1/5)
我的回答
这个所谓三色标记算法,其实是在 CMS 的垃圾回收器阶段开发使用的。其核心作用是标记对象的存活方式。如果一个对象初始存活创建,做一个标记。当这个对象它首次被访问的时候,但是没有确定它最终的结果是存活还是不存活的时候,做一个标记,应该是灰色吧。最开始是黑色,然后是灰色,然后是白色。如果这个对象标记完确认不能存活,就标记为黑色。应该是白色,初始默认是黑色,标记为白色。这样就可以判定这个对象是否还存活,是否有价值。如果一个对象发生了引用的变化,它会重新重置。整个链路过程就是初始为黑,然后扫描,扫描到它,它如果是没有引用,跟直达的引用,就把它标记为黑,标标记为白色。如果它还有引用,就标记为灰色。最终确认这些对象所有对象的状态,然后对统一的标记完没有引用的颜色的,没有引用的对象进行统一的回收,就是三色标记算法。
点评
优点:
- 提到了三色标记在 CMS 等并发 GC 中使用(正确的定位)
- 提到了颜色变化与引用扫描相关的直觉
- 直觉意识到"引用变化要重新标记"(这实际对应写屏障机制)
不足:
- 三色定义完全错误(这是致命问题):
- 正确:白色 = 未被访问/初始状态、灰色 = 已访问但子节点未全扫描、黑色 = 已访问且子节点扫描完毕
- 用户说反了:说"初始是黑色"、"扫描后是白色"、"没引用标白色"
- 回收对象搞反:三色标记的白色对象 = 垃圾(回收对象),不是"没有引用的对象标白然后回收"——白色是初始状态,如果没被扫描到,就是垃圾;用户理解为"标白后再回收",逻辑反了
- 灰色含义不清:灰色是"待扫描子节点"的中间状态,用户理解为"没有确定存活与否"——方向对但术语不准
- 完全没有提并发场景的内存安全——这才是三色标记存在的意义!写屏障修复"黑指白"漏回收问题、读屏障修复"白指黑"误回收问题,全部遗漏
- 与 ZGC/Shenandoah 的关联缺失:ZGC 用读屏障+多映射、Shenandoah 用写屏障+Brooks 转发,都是三色标记的应用
- 没有讲最终状态:三色标记结束 → 灰色必为 0 → 白色 = 待回收
补充:
- 三色严格定义:
- 白色:从未被 GC 访问过,可能是垃圾也可能是存活的(初始状态)
- 灰色:已被访问,但引用的对象还有未访问的(工作队列)
- 黑色:已访问,且所有引用都已扫描完(确定存活)
- 算法流程:GC Roots 全染色黑 → 遍历黑色节点的引用 → 引用对象由白变灰、加入队列 → 灰节点扫描子节点 → 子节点变灰、灰节点变黑 → 灰队列为空时结束
- 并发问题(重点):
- 对象消失(漏回收):黑→白删除 + 灰→黑断链 → 白节点无人引用但活着(黑指白)
- 对象复活(误回收):黑←白添加 + 白→灰断链 → 黑节点仍存活但被错认为垃圾
- 两种修复方案:
- 写屏障(CMS、G1、ZGC 传统模式):修复黑指白,规则"对象引用变白→父对象重染灰"
- 读屏障(Shenandoah、ZGC 现代模式):修复白指黑,规则"读对象指针时检查有效性,无效则重新标记为黑"
- ZGC 的启示:STW 只有 Initial Mark 和 Final Relocate 两个瞬间,靠读屏障+多映射让对象移动不改引用
完整讲解
第一部分:知识重点剖析
核心考点定位:考察三色标记的严格定义+并发场景的内存安全问题+修复机制(写屏障/读屏障)。
知识点拆解:
- 三色严格定义(HotSpot 采用)
- 白色(White):未被 GC 访问过,可能是垃圾也可能是存活(初始状态)
- 灰色(Gray):已被访问,但引用的对象还有未访问(工作队列 / 待扫描)
- 黑色(Black):已访问,且所有引用都已扫描完毕(确定存活)
- 算法流程
1. 所有对象初始为白色
2. GC Roots 全部染黑
3. 取一个黑节点,扫描其引用:
- 引用的对象若为白 → 染灰、加入工作队列
- 该黑节点所有引用扫描完 → 完成
4. 从工作队列取灰节点:
- 扫描其引用,白→灰、入队
- 所有引用扫描完 → 染黑
5. 工作队列为空时结束
6. 剩余白色对象 = 垃圾,回收
不变式(正确性条件):
- 规则 1:黑不能指白(否则漏回收)
- 规则 2:灰不能指黑(否则可能误回收,因为灰节点扫描时看不到已染黑的子节点)
- 并发场景的两大问题(重点!)
三色标记的核心动机是并发标记——让 GC 与用户线程并发扫描,减少 STW。但并发带来两个引用变动:
- 对象消失(漏回收):黑节点 X 原本引用白对象 Y,扫描后删链;紧接着用户线程创建黑节点 Z 引用 Y;此时 Y 是白,被误回收 → Y 活着但被回收
- 对象复活(误回收):黑节点 X 引用白对象 Y 时,用户线程先创建白节点 Z 引用 X,扫描完 Y 后删掉 X→Y 链;此时 Y 被误认为白垃圾,但通过 Z→X→? 关系其实……(更常见是误回收:黑节点被引用后仍被错认为垃圾)
准确表述:
- 黑指白(Broken by Black→White add):漏回收
- 白指黑(Broken by White→Black add):误回收
- 两种修复方案
| 修复方案 | 屏障类型 | 使用者 | 核心思想 |
|---|
| 写屏障 | Write Barrier | CMS、G1、ZGC(经典) | 修复"黑指白":对象引用变白 → 父对象重染灰 |
| 读屏障 | Read/Load Barrier | Shenandoah、ZGC(现代) | 修复"白指黑":读指针时检查有效性,无效则重新标记 |
- ZGC 的启示
- 多映射(Multi-Mapping):同一虚拟地址映射多个物理地址,移动对象时改映射而非引用
- 读屏障:读取指针时检查指针有效性(Validated Address)
- STW 只有两瞬间:Initial Mark 和 Final Relocate,都 <1ms
- 三色标记 + 读屏障 = ZGC 的核心技术组合
- CMS 的具体流程中的三色标记
- Initial Mark(STW):把 GC Roots 染黑
- Concurrent Mark:三色并发扫描(灰→黑、白→灰)
- Remark(STW):处理并发期间的引用变动,靠写屏障让变动可追踪
第二部分:考察方向与延伸递进
考察方向:
- 深度:三色定义准确表述、内存安全问题的严格推理、写屏障具体规则
- 广度:写屏障 vs 读屏障的取舍、ZGC/Shenandoah 的现代方案
- 应用:Remark 如何缩短、CMS/G1/ZGC 的三色标记实现差异
延伸递进面试题:
- 三色标记的"对象消失"问题具体怎么走通?
- 解答要点:黑节点 X 引用白节点 Y → 扫描 X 完成后断链 X→Y → 用户线程创建黑节点 Z 引用 Y → Y 变灰再变黑 → 结束遍历 Y 是黑(正常);但如果 Y 从未被访问,Y 保持白 → 被误回收
- 写屏障具体怎么处理?
- 解答要点:写屏障在对象引用赋值时插入的检查:如果新引用指向白对象,把父对象重染灰;HotSpot 中通过
-XX:+CMSClassUnloadingEnabled 相关逻辑实现
- 为什么 ZGC 从写屏障转向读屏障?
- 解答要点:写屏障频繁触发(每次赋值)影响吞吐;读屏障只在读取指针时检查,对代码侵入小;配合多映射实现"移动对象不改引用"
- Shenandoah 的 Brooks 转发是什么?
- 解答要点:对象头存转发指针,读屏障检查指针有效性时顺带完成对象移动;STW <10ms
- 三色标记和可达性分析的关系?
- 解答要点:三色标记是可达性分析的并发实现,本质都是从 GC Roots 遍历;区别在是否并发、如何处理引用变动
第三部分:面试者参考回答(300-600 字口语化)
面试官您好,三色标记算法是并发 GC 的核心算法,让 GC 扫描和用户线程并发运行,减少 STW。
三色严格定义(HotSpot 采用):
- 白色:从未被 GC 访问过,可能是垃圾也可能是存活(初始状态)
- 灰色:已被访问,但引用的对象还有未访问的(工作队列,待扫描)
- 黑色:已访问,且所有引用都扫描完毕(确定存活)
算法流程:GC Roots 全部染黑 → 取灰节点扫描子节点,白节点变灰入队,灰节点扫描完变黑 → 工作队列为空时结束 → 剩余白色对象就是垃圾,回收。
核心思想:白色 = 潜在垃圾,黑色 = 确定存活,灰色 = 中间态。
并发带来的两个内存安全问题:
- 对象消失(漏回收):黑节点 X 引用白节点 Y → 扫描完成断链 X→Y → 用户线程创建黑节点 Z 引用 Y → 此时 Y 从未被访问保持白,被误回收。这违反"黑不能指白"规则。
- 对象复活(误回收):白节点变黑后仍被断链,导致实际存活的对象被标成白。这违反"灰不能指黑"规则。
两种修复方案:
- 写屏障(Write Barrier):修复"黑指白"问题。规则是当对象引用被赋值时,如果新引用指向白对象,把父对象重染灰。使用者:CMS、G1、ZGC 经典模式。
- 读屏障(Read/Load Barrier):修复"白指黑"问题。规则是读指针时检查有效性,无效则重新标记为黑。使用者:Shenandoah、ZGC 现代模式。
为什么 ZGC 从写屏障转向读屏障? 写屏障每次赋值都触发,影响吞吐;读屏障只在读取指针时检查,对代码侵入小。ZGC 配合多映射(同一虚拟地址映射多物理地址,移动对象改映射而非改引用)实现 STW <1ms。
CMS 中的三色标记:Initial Mark(STW)染黑 GC Roots → Concurrent Mark 并发三色扫描 → Remark(STW)靠写屏障追踪并发期间的引用变动。
一句话总结:三色标记是可达性分析的并发实现——本质都是从 GC Roots 遍历,但通过三色状态管理并发下的引用变动,靠写屏障或读屏障保证内存安全。