java虚拟机-三色标记算法的实现细节,如何解决漏标问题?

109 阅读4分钟

一、三色标记的基本原理

  • 颜色定义

    • 白色:未被访问的对象(初始状态,可能是垃圾)。
    • 灰色:已被访问,但其子对象(引用对象)尚未检查。
    • 黑色:已被访问,且所有子对象已检查(确认存活)。
  • 标记流程

    1. 初始阶段:所有对象标记为白色,根对象(如栈、静态变量)标记为灰色。
    2. 并发标记:GC线程遍历灰色对象,将其子对象标记为灰色,自身标记为黑色。
    3. 完成标记:当灰色对象队列为空时,剩余白色对象即为垃圾。

二、漏标问题的成因

在并发标记过程中,用户线程可能修改对象引用关系,导致以下两种条件同时满足时出现漏标:

  1. 条件1:赋值器插入一条或多条从黑色对象到白色对象的新引用。
  2. 条件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频繁
G1SATB可预测停顿,适合大堆内存内存占用较高
ZGC颜色指针+SATB亚毫秒级停顿,支持TB级堆需要特定硬件支持(如多映射)

六、总结

  • 三色标记通过颜色状态和并发遍历实现高效标记。
  • 漏标问题通过增量更新或SATB策略解决,分别破坏漏标的两个必要条件。
  • 写屏障是关键机制,平衡并发性能与标记准确性。
  • 不同垃圾回收器根据场景选择合适的策略,以优化吞吐量或降低延迟。