在 ZGC 的垃圾回收体系中,并发整理阶段允许回收线程将存活对象从旧地址复制到新地址,而应用线程仍在持续运行并访问这些对象。如果不加干预,应用线程可能通过旧指针读到已经失效的内存,或者读到尚未完成复制的中间状态。ZGC 给出的解法是在“读取对象引用”这个动作上插入一层名为读屏障(Read Barrier)的拦截逻辑,使得每次从堆里取出一个引用时,都会先检查该引用背后的对象是否正处于移动中,必要时将其修正为新地址,从而保障变量访问的一致性。

读屏障的底层原理与着色指针协同
ZGC 的读屏障并不是孤立存在的,它和着色指针(Colored Pointers)设计深度绑定。在 ZGC 中,一个对象引用本身并不只是纯地址,而是借由 64 位指针的高几位保存了元数据标记,例如“是否已重定位”“是否已被标记”等。当应用代码执行类似 Object o = obj.field; 这样的引用加载操作时,JIT 编译器会在字节码层面插入读屏障指令。此时 CPU 拿到带有颜色位的指针,读屏障逻辑会判断颜色位是否表明该对象所在页正处于并发移动状态。
如果颜色位显示对象还在旧页且尚未完成转移,读屏障会触发一次“自愈”动作:它不仅把当前线程拿到的引用修正为新地址,还会把原对象头或引用字段中的旧值改写为新地址,这样后续其他线程再读时就不需要重复处理。这种机制让读屏障的开销被均摊到日常的对象访问中,而不是在 GC 时集中爆发。下面的伪代码展示了读屏障在加载引用时的基本判断逻辑:
// 伪代码:ZGC 读屏障在加载引用时的处理逻辑
Object loadReference(Object holder, long offset) {
// 从堆中读取原始着色指针
long coloredPtr = unsafe.getLong(holder, offset);
// 提取颜色位,判断是否处于待重定位状态
if (isRemapped(coloredPtr) == false) {
// 获取对象对应的转移表项
long newPtr = forwardTable.get(coloredPtr);
if (newPtr != 0) {
// 自愈:写回新地址,后续访问直接命中
unsafe.putLong(holder, offset, newPtr);
coloredPtr = newPtr;
}
}
return toObject(coloredPtr);
}
从实现角度看,读屏障比传统写屏障更“主动”。写屏障通常是在对象被修改时记录脏数据,而读屏障是在对象被使用时立即确认其有效性。这也意味着 ZGC 不需要像 G1 那样维护复杂的卡表来追踪跨代引用,因为只要发生读取,位置正确性就由屏障当场保证。代价则是每次引用读取都有少量指令开销,但在现代 JIT 优化下,这种开销通常可以控制在个位数百分比以内。
并发整理期间的一致性保障场景
考虑一个典型场景:线程 A 正在遍历一个链表,节点对象被 ZGC 后台线程决定从页 X 搬到页 Y。在线程 A 读取下一个节点的引用时,读屏障发现该引用仍指向旧页且处于“未重映射”状态,于是立刻查转移表得到新地址,并把链表里的指针字段也更新成新地址。线程 A 拿到的就是新节点对象,完全感知不到底层发生了移动。若没有读屏障,线程 A 可能访问到页 X 中被回收器清空的内容,造成数据错乱甚至崩溃。
另一个容易忽视的场景是本地变量与寄存器缓存。JIT 可能把某个引用缓存在寄存器中,此时不会每次都走读屏障。ZGC 的处理方式是:在 Safepoint 或特定同步点,寄存器中的引用也会被修正;同时由于并发整理遵循“旧页对象在转移完成前依然可读”,即使短时间用了旧引用,也不会立即出错,只是下次从内存重新加载时屏障会再次纠正。下面的代码展示了多线程下不同线程通过读屏障获得一致视图的过程:
class Node {
Node next;
int value;
}
// 线程1:并发遍历
void traverse(Node head) {
Node cur = head;
while (cur != null) {
// 此处读取 cur.next 会经过读屏障
// 若 next 所指对象被搬移,屏障自动修正
Node next = cur.next;
System.out.println(cur.value);
cur = next;
}
}
// 线程2:ZGC 后台并发整理,移动 Node 对象
// 应用层无需感知,读屏障屏蔽了地址变化
这种一致性模型属于“物理地址变化、逻辑引用稳定”。对开发者来说,Java 语义层面的对象身份(== 比较、哈希码)不会因为 GC 移动而改变,因为 ZGC 保证对象身份固定在转移后的新地址,旧地址只是临时视图。相比某些需要暂停所有线程才能移动的收集器,ZGC 的读屏障让应用真正实现了大部分时间无感运行。
性能特征与工程实践注意点
读屏障虽然解决了并发整理的一致性难题,但它并非零成本。在引用密集的算法中,例如大量指针追踪的图遍历、序列化反序列化,读屏障的累计调用次数非常高。生产环境观测显示,这类负载下 ZGC 的读屏障可能带来 5% 到 15% 的吞吐下降,换取的是亚毫秒级的暂停时间。因此是否选用 ZGC,要看业务更怕停顿还是更怕吞吐损耗。
工程上可以通过减少不必要的引用读取来降低屏障开销,比如使用基本类型数组代替对象数组、在热点循环中缓存局部引用。同时 JVM 提供了 -XX:+UnlockExperimentalVMOptions 与 ZGC 相关诊断参数,可以输出屏障命中统计。以下片段演示如何通过 JFR 或日志关注读屏障相关的重映射次数:
# 启动 ZGC 并开启相关诊断输出
java -XX:+UseZGC
-Xlog:gc*
-XX:+UnlockDiagnosticVMOptions
-XX:ZStatisticsInterval=10
-jar app.jar
值得注意的是,读屏障只保障“引用读取”的一致性,不替代锁或原子类提供的并发语义。如果多个线程同时修改对象字段,依然需要 synchronized 或 java.util.concurrent 工具。读屏障解决的是 GC 移动对象和线程访问对象之间的底层视图冲突,而不是业务层面的竞争条件。理解这一边界,才能在架构设计时正确评估 ZGC 的能力范围,避免把 GC 机制误当作并发控制手段。
ZGCRead_BarrierConcurrent_Compaction修改时间:2026-08-16 00:04:33