写屏障和记忆集共同解决分代GC中跨代引用导致的漏标问题:写屏障在老→年引用写入时标记脏卡,记忆集据此聚合存储跨代引用位置,供年轻代GC快速定位并扫描外部根。

写屏障(Write Barrier)和记忆集(Remembered Set,简称 RSet)是垃圾回收器(尤其是分代式 GC,如 G1、ZGC、Shenandoah)中维护跨代引用关系的核心机制。它们共同解决一个关键问题:当年轻代对象被老年代对象引用时,如何在年轻代 GC 时不漏掉这些“外部根”,避免错误回收。
为什么需要写屏障和记忆集
分代 GC 将堆划分为年轻代(Young Gen)和老年代(Old Gen)。年轻代 GC(Minor GC)频率高、范围小,只扫描年轻代对象和 GC Roots(如栈帧、静态变量等)。但若老年代对象持有对年轻代对象的引用,该引用不会出现在年轻代的局部扫描范围内——它是个“跨代引用”,必须被识别并加入扫描根集合,否则年轻代对象可能被误回收。
写屏障在每次发生“老→年”引用写入时拦截并记录;记忆集则持久化存储这些跨代引用的粗粒度位置信息(例如:哪个老年代区域可能指向了哪块年轻代区域),供 GC 快速定位需扫描的外部根。
写屏障如何捕获跨代写操作
写屏障是一段嵌入在赋值语句(如 obj.field = youngObj)前后的底层代码(通常由 JIT 编译器插入),用于监控引用字段的修改。针对跨代引用,常用的是卡表写屏障(Card Table Write Barrier)或更精细的字节码级/内存屏障级写屏障(如 G1 的 Post-Write Barrier)。
- 当线程执行 oldObj.field = youngObj(老年代对象引用年轻代对象)时,写屏障被触发
- 屏障计算 oldObj 所在内存页(或“卡页”,card,通常 512B)的索引,并将对应卡表(Card Table)条目标记为 dirty
- 这个 dirty 卡表示“该内存区域中可能存在指向年轻代的引用”,后续 GC 可据此快速筛选出需扫描的老年代候选区域
记忆集如何组织与使用
记忆集是写屏障记录结果的聚合结构,本质是一个反向索引表:以“被引用的区域”(如某块年轻代 region)为键,存储“哪些老年代区域(或卡页)包含指向它的引用”。G1 中每个 region 都维护自己的 RSet。
- RSet 不直接存具体字段地址,而是按“源区域粒度”(如 card、region)记录,兼顾精度与开销
- 年轻代 GC 开始前,GC 线程扫描所有 dirty 卡,解析其中的对象引用,过滤出真正指向本次 GC 范围内年轻代对象的引用,并将其加入根集合(Roots)
- G1 还支持并发 RSet 更新:后台线程将写屏障暂存的引用日志(dirty card queue)批量合并进 RSet,减少 Stop-The-World 时间
实际实现中的关键细节
不同 GC 算法对写屏障和 RSet 的设计有差异,但核心逻辑一致:
- 写屏障类型影响性能与精度:如 G1 使用 Post-Write Barrier(写后屏障)+ SATB(Snapshot-At-The-Beginning)配合处理并发修改;ZGC 使用读屏障(Load Barrier)为主,弱化写屏障依赖
- RSet 维护有成本:RSet 本身占用堆外内存,更新 RSet(尤其并发合并)消耗 CPU;因此 GC 会限制 RSet 容量,对高频跨代引用区域启用“全区域扫描”作为兜底
- 并非所有写都需要屏障:JVM 会对栈上局部变量、新生代内引用等做逃逸分析或区域判断,绕过不必要的屏障,降低运行时开销

















