写屏障和卡表协同实现分代GC的高效跨代引用追踪:写屏障在老年代对象引用年轻代对象时触发,通知系统记录跨代写操作;卡表以粗粒度标记对应内存页为脏,GC仅扫描脏页中的老年代对象,大幅减少扫描范围。

写屏障和卡表是分代垃圾回收中协同工作的两个底层机制,核心目标只有一个:让年轻代 GC 不用扫描整个老年代,就能准确找到所有可能引用年轻代对象的老年代对象。
写屏障负责捕获跨代写操作
它不是在每次赋值时都无差别拦截,而是有明确触发条件:只有当老年代对象的某个字段被赋值为一个年轻代对象时,才激活。比如 objA.child = youngObj,其中 objA 在老年代、youngObj 在新生区,此时写屏障就会介入。
- 它不记录具体引用关系,只做一件事:通知系统“这里发生了老→年的引用变更”
- 在 V8 或 HotSpot 中,它以编译期插入的轻量汇编指令形式存在,嵌入在赋值语句前后
- 对 JavaScript 或 Java 开发者完全透明,无需、也不应手动干预
- 年轻代对象引用老年代对象,不会触发写屏障——因为这不影响年轻代 GC 的正确性
卡表是写屏障作用的落脚点
卡表本质是一个字节数组,把堆内存按固定大小(如 512 字节)切分成若干“卡页”,每个字节对应一页。当写屏障检测到跨代写入,就计算出目标对象所在卡页的索引,并把对应字节标记为“脏”(dirty)。
- 例如地址 0x123456 经右移 9 位(÷512)得索引 i,CARD_TABLE[i] = 1
- 它不关心具体哪个对象、哪个字段,只标记“这一块内存里至少有一个跨代引用”
- 这种粗粒度设计大幅节省空间,避免为每个跨代引用单独建表
- 卡表本身不直接参与 GC 根扫描,而是作为记忆集(Remembered Set)的数据源
二者配合实现高效跨代扫描
年轻代 GC 开始前,回收器不遍历整个老年代,而是先扫描卡表中所有“脏”的卡页,定位出这些卡页内的老年代对象,再只检查它们的引用字段是否指向年轻代。
- 实测显示,在大型应用中,90% 以上老年代对象不含跨代引用,真正需要检查的不足 5%
- 记忆集就是由脏卡解析而来,是卡表的逻辑聚合,用于快速构建 GC Roots 子集
- G1 和 V8 都采用类似思路,只是 G1 每个 Region 自带卡表,而 HotSpot 全局一张
- 写屏障若缺失,就会漏标——本该存活的年轻代对象被误回收;卡表若精度太粗,则会多扫——降低效率但不破坏正确性
常见误区与实际影响
写屏障不是内存屏障(Memory Barrier),前者是 GC 层逻辑机制,后者是 CPU 层指令约束;卡表也不是哈希表或链表,它就是一个极简的索引数组。
- 高频修改老年代对象字段会轻微拖慢写性能,但现代引擎已通过条件跳转、延迟标记优化
- 多个线程同时标记相邻卡位可能引发伪共享(false sharing),可通过卡位对齐缓解
- 同代引用(如老→老、年→年)从不触发写屏障,因为 GC 扫描本代时自然覆盖全部对象
- 卡表大小固定,随堆增长线性扩展,但远小于堆本身,典型 HotSpot 卡表仅占堆内存约 0.1%

















