一、三色标记的基本原理
-
颜色定义:
- 白色:未被访问的对象(初始状态,可能是垃圾)。
- 灰色:已被访问,但其子对象(引用对象)尚未检查。
- 黑色:已被访问,且所有子对象已检查(确认存活)。
-
标记流程:
- 初始阶段:所有对象标记为白色,根对象(如栈、静态变量)标记为灰色。
- 并发标记:GC线程遍历灰色对象,将其子对象标记为灰色,自身标记为黑色。
- 完成标记:当灰色对象队列为空时,剩余白色对象即为垃圾。
二、漏标问题的成因
在并发标记过程中,用户线程可能修改对象引用关系,导致以下两种条件同时满足时出现漏标:
- 条件1:赋值器插入一条或多条从黑色对象到白色对象的新引用。
- 条件2:赋值器删除了所有从灰色对象到该白色对象的引用。
示例场景:
// 初始状态:A(黑)、B(灰)、C(白)
A.ref = C; // 条件1:黑→白
B.ref = null; // 条件2:删除灰→白
此时,C未被标记为存活,最终会被错误回收。
三、解决方案:破坏漏标条件
1. 增量更新(Incremental Update)
- 核心思想:破坏条件1,确保黑→白的新引用会被重新扫描。
- 实现方式:
- 写屏障(Write Barrier):当用户线程将黑色对象指向白色对象时,触发屏障逻辑。
- 黑→灰回退:将黑色对象重新标记为灰色,后续重新扫描其子对象。
- 应用场景:CMS垃圾回收器。
代码逻辑示例:
void writeBarrier(Object obj, Object field, Object newValue) {
if (isBlack(obj) && isWhite(newValue)) {
// 将黑色对象回退为灰色
markGray(obj);
}
obj.field = newValue; // 实际赋值
}
2. 原始快照(Snapshot At The Beginning, SATB)
- 核心思想:破坏条件2,以标记开始时的引用关系快照为依据。
- 实现方式:
- 写屏障记录旧引用:当用户线程删除引用时,记录被删除的引用目标(白色对象)。
- 按快照标记:无论后续引用如何变化,均按快照时的引用关系标记存活对象。
- 应用场景:G1、Shenandoah垃圾回收器。
代码逻辑示例:
void writeBarrier(Object obj, Object field, Object oldValue) {
if (oldValue != null && isWhite(oldValue)) {
// 记录被删除的引用目标
addToSATBQueue(oldValue);
}
obj.field = newValue; // 实际赋值
}
四、三色标记的实现优化
1. 并发标记阶段的写屏障
- 目的:在用户线程修改引用时,维护标记的正确性。
- 开销:写屏障会增加少量运行时开销,但避免Stop-The-World(STW)。
2. 标记队列管理
- 灰色队列:保存待扫描的灰色对象,GC线程并发处理。
- SATB队列:记录因引用删除可能被漏标的白色对象。
3. 最终标记(Remark)阶段
- STW暂停:短暂暂停用户线程,处理残余灰色对象和SATB队列。
- 确保完整性:修正并发阶段可能遗漏的存活对象。
五、不同垃圾回收器的选择
| 回收器 | 策略 | 优点 | 缺点 |
|---|---|---|---|
| CMS | 增量更新 | 低延迟,适合老年代回收 | 内存碎片化,Full GC频繁 |
| G1 | SATB | 可预测停顿,适合大堆内存 | 内存占用较高 |
| ZGC | 颜色指针+SATB | 亚毫秒级停顿,支持TB级堆 | 需要特定硬件支持(如多映射) |
六、总结
- 三色标记通过颜色状态和并发遍历实现高效标记。
- 漏标问题通过增量更新或SATB策略解决,分别破坏漏标的两个必要条件。
- 写屏障是关键机制,平衡并发性能与标记准确性。
- 不同垃圾回收器根据场景选择合适的策略,以优化吞吐量或降低延迟。