解决对象消失的问题
ZGC 确实需要处理并发标记期间因用户线程修改引用导致的 对象消失问题(即对象实际存活但被误回收),但其解决方案并非依赖 STW(停顿),而是通过 读屏障(Load Barrier)的增量标记机制 实现无停顿处理。以下是 ZGC 的完整应对策略:
⚡ 一、问题本质:并发标记中的对象消失
并发标记期间,用户线程修改引用可能导致以下两类问题:
- 对象漏标(误回收):
- 场景:用户线程将对象 A 的引用写入对象 B 的字段(
B.field = A),但此时标记线程已扫描过 B,导致 A 未被标记。 - 风险:A 被误回收 → 程序崩溃。
- 场景:用户线程将对象 A 的引用写入对象 B 的字段(
- 浮动垃圾(多回收):
- 场景:用户线程删除对象 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; // 返回对象地址
}
工作流程:
- 标记阶段:
- 当应用线程读取对象引用(如
obj = ref.field)时,触发读屏障。 - 屏障检查该对象是否已被标记:
- 若未标记 → 立即将其标记为存活(原子操作)。
- 若已标记 → 直接返回。
- 当应用线程读取对象引用(如
- 修正效果:
- 所有被访问的对象(即存活的强引用对象)都会被标记,确保不会误回收。
- 未被访问的对象可能是垃圾(即使被漏标),可安全回收。
优势:
- 无 STW:标记修正零停顿,由应用线程在访问对象时触发。
- 精准性:仅标记实际被访问的对象(强存活性),避免过度标记。
- 低开销:读屏障逻辑经 JIT 编译优化,单次触发仅增加 5–10ns 开销。
🔄 三、对比传统方案:SATB vs 增量更新
| 方案 | 原理 | 停顿 | 适用 GC |
|---|---|---|---|
| SATB(快照隔离) | 标记开始时冻结引用关系(STW 记录快照),后续变化视为浮动垃圾。 | 有 | G1、Shenandoah |
| 增量更新(ZGC) | 通过读屏障实时修正新增引用,确保存活对象被标记。 | 无 | ZGC |
ZGC 选择增量更新的原因:
- SATB 需初始 STW 记录快照(堆越大停顿越长),而 ZGC 追求 全程无 STW。
- 增量更新仅标记实际访问的对象,比 SATB 的“可能存活”更精确(减少浮动垃圾)。
💎 四、设计总结
- 问题:并发标记期间引用变化 → 对象漏标风险。
- 解决方案:
- 读屏障实时标记:访问对象时检查并补标(增量更新)。
- 不依赖 STW:无暂停阶段处理增量引用。
- 效果:
- 零误回收(程序安全)。
- 亚毫秒级停顿(与堆大小无关)。
性能代价:读屏障引入约 5% 的性能开销(实测),换取确定性低延迟。
⚠️ 注意事项
- 浮动垃圾可接受:删除引用导致的浮动垃圾不影响安全性,下次回收即可清理。
- 屏障优化:ZGC 的读屏障通过 硬件加速(如 Intel TSX)和 JIT 内联降低开销。
- 调优建议:对读屏障敏感的应用(如高频指针访问),可通过
XX:+UseFastLoadBarrier启用快速屏障模式(JDK 21+)。
ZGC 通过 无 STW 的读屏障增量标记 彻底解决了并发标记中的对象消失问题,这是其实现 TB 级堆下亚毫秒停顿的核心创新之一。