bigywater.tech
Java · 176

写屏障与卡表:GC 如何知道哪些对象被修改

GC 怎么知道老年代里哪些对象在引用年轻对象?答案是写屏障和卡表,它们藏在每一次引用赋值里。这篇先验证「老年代指向年轻对象」确实能让它活下来,再测不同回收器下引用存储与 int 存储的耗时差(G1 的记忆集见前面一篇)。

5 次 GC 后仍在
老年代对象持有的年轻对象在年轻代 GC 后的存活情况
0.52 vs 1.124 ns
Serial 与 G1 的引用存储耗时(ns/次)
0.379 ns
int 存储耗时(没有屏障)

老年代指向年轻对象

实测 · 本机执行输出
  老年代对象 Holder 持有一个年轻对象(没有别的引用):经历 5 次年轻代 GC 之后,弱引用是否还在:true
  把 Holder.ref 置为 null 之后再经历 3 次年轻代 GC:弱引用是否还在:false(Holder 还活着:true)

屏障的代价

实测 · 本机执行输出
回收器               int 存储      引用存储      引用读取       引用存储 ÷ int 存储
Serial             0.379     0.520     0.460                1.37
Parallel           0.381     0.519     0.466                1.36
G1                 0.412     1.124     0.841                2.73
ZGC                0.410     0.756     0.716                1.84

分步演示

写屏障
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;回收器 Serial / Parallel / G1 / ZGC,堆 1 GB
    • 没有验证:卡表本身(卡片大小、脏卡的数量,没有直接观察)、G1 的 SATB 前屏障与后屏障各自的耗时、ZGC 的加载屏障细节、JIT 对屏障的消除(逃逸分析、新对象免屏障)、数组存储以外的字段存储;耗时是自写循环的一次运行
    京ICP备2026067502号-1京公网安备11010502063487号