Card Table 是用于标记可能含跨代引用的脏页而非定位引用,以512字节为粒度映射老年代,写屏障触发其标记,Minor GC仅扫描dirty卡页并结合RSet识别真实跨代引用,漏写屏障将导致漏标与对象误回收。

Card Table 不是用来“定位引用”的,而是标记“可能有跨代引用的内存区域”。它本身不存任何引用地址,只做粗粒度脏页标记——这是理解 JVM 如何高效处理跨代引用的核心前提。
卡表本质是 512 字节粒度的脏页索引
HotSpot 将老年代按每 512 字节划分为一个“卡页”(Card Page),每个卡页对应 Card Table 中的一个 byte 元素。初始值全为 0(clean)。只要老年代中某个对象字段被赋值为指向年轻代对象(如 obj.field = youngObj),写屏障就会触发:
- 计算该对象地址在卡表中的索引:(uintptr_t)obj >> 9(等价于地址除以 512)
- 将对应 byte 设为非零值(如 0xff),标记为 dirty
- 这个动作只对“老→年轻”写入生效;其他方向(如年轻→老、老→老)不触发
Minor GC 只扫描 dirty 卡页,而非整个老年代
每次 Minor GC 开始前,JVM 不会遍历全部老年代对象,而是:
- 遍历 Card Table 数组,找出所有值非零的位置
- 对每个 dirty 卡页所覆盖的 512 字节内存区域,逐个检查其中的对象字段
- 确认哪些字段真实引用了年轻代对象,并将其加入 GC Roots
这种策略把扫描范围从 GB 级老年代压缩到 KB 级脏页集合,停顿时间从百毫秒级降至几毫秒。
Card Table 是线索生成器,Remember Set 才是引用登记表
Card Table 本身不记录谁引用了谁,只是提供“哪里可能有线索”的提示。真正维护跨代引用关系的是 Remember Set(RSet):
- RSet 是异步构建的:Refinement 线程定期从 Dirty Card Queue 中取出 dirty 卡页
- 扫描这些卡页内对象,用 OopMap 快速识别出跨代字段
- 把源对象(老年代)和目标区域(如某块 Survivor)的关系登记进对应区域的 RSet
- Minor GC 实际使用的 GC Roots = 传统根 + 年轻代对应 RSet 中的所有条目
绕过写屏障会导致漏标,GC 可能误回收
Card Table 的有效性完全依赖写屏障。如果通过 JNI 或 Unsafe 直接修改内存,跳过 JVM 的写屏障逻辑:
- 即使发生了老→年轻引用,卡表也不会被标记为 dirty
- Minor GC 就不会扫描该卡页,从而漏掉真实存活对象
- 结果是年轻代对象被错误回收,程序出现 NullPointerException 或数据损坏
这说明 Card Table 不是独立运行的机制,而是写屏障、卡表、RSet、GC 扫描四者协同工作的关键一环。

















