解决对象消失的问题

147 阅读3分钟

解决对象消失的问题

ZGC 确实需要处理并发标记期间因用户线程修改引用导致的 对象消失问题(即对象实际存活但被误回收),但其解决方案并非依赖 STW(停顿),而是通过 读屏障(Load Barrier)的增量标记机制 实现无停顿处理。以下是 ZGC 的完整应对策略:


一、问题本质:并发标记中的对象消失

并发标记期间,用户线程修改引用可能导致以下两类问题:

  1. 对象漏标(误回收)
    • 场景:用户线程将对象 A 的引用写入对象 B 的字段(B.field = A),但此时标记线程已扫描过 B,导致 A 未被标记。
    • 风险:A 被误回收 → 程序崩溃。
  2. 浮动垃圾(多回收)
    • 场景:用户线程删除对象 C 的唯一引用,但标记线程已标记 C 为存活。
    • 风险:C 成为浮动垃圾 → 内存浪费(无害,下次回收即可)。

🛡️ 二、ZGC 的解决方案:读屏障增量标记

ZGC 不依赖 STW 处理增量更新,而是通过 读屏障(Load Barrier) 在应用线程访问对象时实时修正标记状态:

// 读屏障伪代码(简化版)
Object load_barrier(Address addr) {
    if (is_marking_active()) {        // 当前处于标记阶段
        if (!is_marked(addr)) {       // 对象未被标记
            mark_object(addr);         // 立即标记为存活
        }
    }
    return addr;  // 返回对象地址
}

工作流程

  1. 标记阶段
    • 当应用线程读取对象引用(如 obj = ref.field)时,触发读屏障。
    • 屏障检查该对象是否已被标记
      • 若未标记 → 立即将其标记为存活(原子操作)。
      • 若已标记 → 直接返回。
  2. 修正效果
    • 所有被访问的对象(即存活的强引用对象)都会被标记,确保不会误回收。
    • 未被访问的对象可能是垃圾(即使被漏标),可安全回收。

优势

  • 无 STW:标记修正零停顿,由应用线程在访问对象时触发。
  • 精准性:仅标记实际被访问的对象(强存活性),避免过度标记。
  • 低开销:读屏障逻辑经 JIT 编译优化,单次触发仅增加 5–10ns 开销。

🔄 三、对比传统方案:SATB vs 增量更新

方案原理停顿适用 GC
SATB(快照隔离)标记开始时冻结引用关系(STW 记录快照),后续变化视为浮动垃圾。G1、Shenandoah
增量更新(ZGC)通过读屏障实时修正新增引用,确保存活对象被标记。ZGC

ZGC 选择增量更新的原因:

  • SATB 需初始 STW 记录快照(堆越大停顿越长),而 ZGC 追求 全程无 STW
  • 增量更新仅标记实际访问的对象,比 SATB 的“可能存活”更精确(减少浮动垃圾)。

💎 四、设计总结

  1. 问题:并发标记期间引用变化 → 对象漏标风险。
  2. 解决方案
    • 读屏障实时标记:访问对象时检查并补标(增量更新)。
    • 不依赖 STW:无暂停阶段处理增量引用。
  3. 效果
    • 零误回收(程序安全)。
    • 亚毫秒级停顿(与堆大小无关)。

性能代价:读屏障引入约 5% 的性能开销(实测),换取确定性低延迟。


⚠️ 注意事项

  • 浮动垃圾可接受:删除引用导致的浮动垃圾不影响安全性,下次回收即可清理。
  • 屏障优化:ZGC 的读屏障通过 硬件加速(如 Intel TSX)和 JIT 内联降低开销。
  • 调优建议:对读屏障敏感的应用(如高频指针访问),可通过 XX:+UseFastLoadBarrier 启用快速屏障模式(JDK 21+)。

ZGC 通过 无 STW 的读屏障增量标记 彻底解决了并发标记中的对象消失问题,这是其实现 TB 级堆下亚毫秒停顿的核心创新之一。