卡表不标记具体引用,而是以512字节为粒度标记老年代中可能含跨代引用的脏卡页;通过写屏障在老→年轻赋值时触发标记,Minor GC仅扫描dirty卡页内对象,从而大幅缩减老年代扫描范围。

卡表(Card Table)本身不标记“老年代引用新生代”,而是标记“老年代中哪些区域可能含有指向新生代的引用”。它的设计目标是:在年轻代 GC(如 Minor GC)时,快速找出所有可能跨代引用的起点,避免扫描整个老年代。
卡表的基本原理
JVM 将老年代(通常是整个堆的一部分)逻辑划分为固定大小的内存块,称为“卡页”(Card Page),常见大小为 512 字节。每张卡页对应卡表中的一个字节(或位)。当老年代中某个对象的字段被赋值为一个新生代对象时(即发生一次跨代写操作),JVM 会通过写屏障(Write Barrier)捕获该操作,并将对应卡页在卡表中的标志位置为 dirty(脏)。
写屏障如何触发卡表标记
关键在于 JVM 在执行类似 obj.field = youngObj 这样的赋值语句时,如果 obj 在老年代、youngObj 在新生代,就会触发写屏障。此时:
- 计算
obj所在地址对应的卡页索引:card_index = (uintptr_t)obj >> card_shift(例如 card_shift=9 表示 512 字节/卡) - 将卡表中
card_table[card_index]设为 dirty(如写入 0xFF) - 后续 Minor GC 时,只扫描所有 dirty 卡页内的对象,检查其是否持有新生代引用
为什么不是“精确标记引用”,而是“粗粒度标记卡页”
卡表是一种空间换时间的折中方案:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
立即学习“Java免费学习笔记(深入)”;
- 不记录具体哪个字段、哪个对象引用了新生代,只记录“这个 512 字节区域里可能有”
- 避免为每次写操作维护精细引用关系,开销可控
- Minor GC 时虽需遍历卡页内所有对象,但远少于全量扫描老年代
- 实际中大多数卡页仍是 clean 的,可跳过
不同垃圾收集器的实现差异
并非所有 GC 都用传统字节数组卡表:
- Serial / Parallel GC:使用基于字节的卡表(byte-map),最典型
- G1 GC:不用传统卡表,改用“记忆集”(Remembered Set, RSet),按 Region 粒度维护引用关系,更精准但开销更高
- ZGC / Shenandoah:使用读屏障 + 指针元数据,基本不依赖卡表
所以,卡表标记的是“老年代中可能持有新生代引用的内存区域”,靠写屏障维护,为 Minor GC 提供高效的老年代扫描边界。它不追踪引用本身,也不保证每个 dirty 卡页都一定存在跨代引用——只是确保不会漏掉任何可能的引用。

















