Shenandoah的读屏障是嵌入字段读取前的轻量跳转逻辑,非额外检查:仅当GC处于并发转移阶段且对象Brooks指针已更新为新地址时,才通过一次原子解引用自动重定向至新副本,全程用户态执行、开销约1–2纳秒。

Shenandoah 的对象访问屏障(主要是读屏障,Read Barrier)不是“额外检查”,而是嵌入在每次对象字段读取前的轻量级跳转逻辑,核心目标是让旧地址始终可用、新副本自动生效。
读屏障触发时机与判断逻辑
JVM 在 JIT 编译或解释执行时,对所有 通过引用访问对象字段 的字节码(如 getfield、aload 后跟字段读取)自动插入读屏障调用。该屏障只在以下条件同时满足时才真正介入:
- 当前 GC 正处于并发转移阶段(即已有对象被复制,且堆中存在已转发对象)
- 所访问对象的 Brooks 指针(fwdptr)不等于对象自身地址(说明它已被复制,且 fwdptr 已 CAS 更新为新地址)
若两个条件都不满足,屏障直接返回原对象引用,无任何间接跳转——现代 CPU 分支预测对此类模式高度优化,开销可忽略。
Brooks 指针如何支撑屏障跳转
每个 Java 对象头前方静态预留 8 字节,作为原子可更新的 forwarding pointer 字段:
- 初始状态:fwdptr 指向对象自身起始地址(即
obj == *fwdptr) - GC 复制完成后:用 CAS 原子地将 fwdptr 更新为新副本地址(
*fwdptr = new_obj) - 读屏障函数(如
read_barrier(oop src))内部仅做一次解引用:return resolve_forwarded(src),而resolve_forwarded实际就是读取并返回*brooks_ptr_addr(src)
这意味着:用户线程哪怕仍在使用栈上或寄存器里的旧引用,只要该对象已被转发,下一次字段读取就会自然落在新副本上,无需修改任何引用值。
为什么不用写屏障处理读一致性
写操作由独立的写屏障(Store Barrier)保障,但读屏障不依赖它——因为读本身不改变状态,只需确保“看到的是最新副本”。关键点在于:
- 转发指针更新是原子的(CAS),不存在中间态;旧地址和新地址的数据在复制完成前均保持完整
- 读屏障只做一次间接寻址,不涉及锁、不修改内存、不触发系统调用,纯用户态指令流
- 即使多个线程并发读同一旧地址,各自屏障都独立跳转,彼此无竞争
这与早期靠内存保护陷阱(trap)实现转发的方式有本质区别:后者需陷入内核、切换上下文,延迟达微秒级;Shenandoah 的读屏障全程在用户态完成,平均开销约 1–2 纳秒。
实际运行中的行为边界
读屏障并非无条件启用,它的生效受 GC 阶段控制:
- 仅在“并发标记”之后、“并发更新引用”之前这段时间活跃(即对象已转发但全局引用尚未批量修正)
- 初始标记(Init Mark)和最终标记(Final Mark)等 STW 阶段虽短,但此时屏障可能被临时禁用以减少干扰
- JIT 编译器会尝试内联屏障逻辑;若因逃逸分析复杂等原因未能内联,性能下降明显,因此生产环境需关注 JIT 日志中 barrier 是否被优化
它不保证“所有访问绝对不慢”,而是把“必须停顿”的代价,换成了“每次读略多一次指针解引用”的确定性开销——这是低延迟设计中典型的权衡选择。

















