卡表是JVM为优化Minor GC而设的粗粒度脏页标记机制,将老年代划分为512字节卡页,每页对应卡表一个字节;写屏障在老→年轻引用时标记对应卡页为dirty;Minor GC仅扫描dirty卡页内对象以识别跨代引用。

Java 中的卡表(Card Table)是一种用于记录老年代对象对新生代对象引用的辅助数据结构,主要服务于垃圾回收器(尤其是 CMS 和 G1 之前的分代 GC,如 Parallel GC、Serial GC)中的写屏障(Write Barrier)机制。它不直接“标记引用”,而是通过粗粒度的内存页(Card)标记来快速识别哪些老年代区域可能含有指向新生代的引用,从而在 Minor GC 时避免全堆扫描。
卡表的基本单位和映射关系
卡表将老年代的内存划分为固定大小的卡页(Card),通常每张卡对应 512 字节(不同 JVM 实现可能略有差异,如 HotSpot 默认为 512B)。整个卡表是一块连续的字节数组,每个字节代表一个卡页的状态:
- 0x00(clean):表示该卡页内暂未发现对新生代的引用;
- 0xFF(dirty):表示该卡页已被写屏障标记为“脏”,即其中至少有一个对象字段被修改过,且目标是新生代对象(或可能是)。
写屏障如何触发卡表标记
当程序执行类似 oldObj.field = youngObj 的赋值操作时,JVM 在运行时插入的写屏障(如 Post-Write Barrier)会拦截该写操作,并做以下事情:
- 计算
oldObj所在内存地址对应的卡表索引:card_index = (uintptr_t)oldObj >> card_shift(card_shift通常是 9,因 2⁹ = 512); - 将卡表中对应位置置为 dirty(例如写入 0xFF);
- 该过程极快,只涉及一次内存写,不检查字段实际是否真的指向新生代(保守标记)。
Minor GC 时如何利用卡表
在每次 Young GC 开始前,GC 线程会扫描整个卡表,找出所有 dirty 的卡页,然后仅遍历这些卡页覆盖的老年代内存区域,从中精确识别出真正指向新生代对象的引用(即“跨代引用”),并将这些新生代对象加入 GC Roots。
立即学习“Java免费学习笔记(深入)”;
- 这大幅缩小了根扫描范围——无需扫描整个老年代;
- 扫描时仍需逐个检查卡页内的对象和引用字段(因为卡表是粗粒度的),但代价远低于全堆扫描;
- GC 完成后,通常会将已处理的 dirty 卡页清零(clean),或采用“懒清理”策略(如 CMS 使用的 card marking stack 延迟处理)。
注意:G1 和 ZGC 已不依赖传统卡表
现代收集器做了优化:
- G1 使用更精细的 Remembered Set(RSet),按 Region 维度维护跨代/跨区引用,精度更高,卡表仅用于初始 RSet 构建的辅助;
- ZGC/Shenandoah 依靠读屏障 + 指针元数据(如 Mark Bit、Finalizable Bit)实现并发 GC,不再使用卡表机制。
所以,卡表本质是一种用空间换时间、以少量内存(约老年代大小 / 512)和一次写屏障开销,换取 Young GC 效率的关键工程取舍。


















