card table 是老年代的脏页索引,仅标记“哪块内存可能有跨代引用”;它将老年代每512字节划为一卡页,对应一个byte元素,初始为0,老→年轻写入时通过写屏障设为非零(dirty),不记录具体引用关系,也不识别字段或对象,只为minor gc提供扫描线索。

Card Table 不记录“谁引用了谁”,它只标记“哪块内存可能有跨代引用”。这是理解它作用的关键前提。
卡表本质是老年代的脏页索引
HotSpot 将老年代按每 512 字节划分为一个卡页(Card Page),每个卡页对应 Card Table 中的一个字节元素。初始值为 0(clean)。只要老年代中某个对象字段被赋值为指向年轻代对象(如 obj.field = youngObj),写屏障就会触发:
- 计算该对象地址在卡表中的索引:(uintptr_t)obj >> 9(即地址除以 512)
- 将对应字节设为非零值(如 0xff),标记为 dirty
这个动作仅对“老→年轻”的写入生效,不关心泛型、数组类型或字段名,也不保存任何引用地址。
它不扫描也不登记,只提供线索
Card Table 本身不维护引用关系,也不识别具体哪个字段、哪个对象发生了跨代引用。它只是告诉 Minor GC:“这一片 512 字节内存里,至少有一个对象可能引用了年轻代”。真正做识别和登记的是 Remembered Set(RSet):
- Refinement 线程定期从 Dirty Card Queue 中取出 dirty 卡页
- 扫描这些卡页内所有对象的字段,用 OopMap 快速识别出真实跨代引用
- 把源对象(老年代)与目标区域(如某块 Survivor)的关系登记进对应 RSet
Minor GC 最终使用的 GC Roots = 传统根 + 年轻代对应 RSet 中的所有条目。
漏掉写屏障就等于漏标
Card Table 的有效性完全依赖写屏障。如果通过 JNI、Unsafe 或反射绕过 JVM 内存访问逻辑:
- 即使发生了老→年轻引用,卡表也不会被标记为 dirty
- Minor GC 就不会扫描该卡页,从而漏掉真实存活对象
- 结果是年轻代对象被错误回收,引发 NullPointerException 或数据损坏
所以 Card Table 不是独立组件,而是写屏障、卡表、RSet、GC 扫描四者协同工作的关键一环。











