卡表是JVM优化老年代到新生代跨代引用扫描的关键机制,将老年代划分为512字节卡页,每页对应卡表一个字节;写屏障在老年代引用新生代时标记对应卡页为脏;Minor GC仅扫描脏卡页内对象,识别跨代引用作为GC Roots。

卡表(Card Table)是JVM中用于优化老年代到新生代跨代引用扫描的关键机制,它避免了对整个老年代进行全量扫描来查找指向新生代的引用,显著提升GC效率。
卡表的基本原理
卡表将老年代内存划分为固定大小的“卡页”(通常为512字节),每个卡页对应卡表中的一个字节。当某块老年代内存中存在指向新生代对象的引用时,对应卡表项被标记为“脏”(如置为1)。GC时只需扫描卡表中所有“脏”的卡页,定位并检查其中的引用,而非遍历全部老年代对象。
写屏障如何维护卡表
卡表的准确性依赖于写屏障(Write Barrier)——在每次发生“老年代对象引用新生代对象”的写操作时,JVM自动触发一段轻量级代码,将目标引用所在卡页对应的卡表项标记为脏。
- 例如:老年代中一个HashMap的value字段被赋值为一个刚创建的String(位于Eden区),写屏障会计算该String地址所属的卡页索引,并设置card_table[index] = 1
- HotSpot使用的是“后写屏障”(Post-write Barrier),在引用写入完成后立即执行卡表标记
- 写屏障开销极小,但能彻底避免漏标,是卡表机制可靠的前提
卡表在Minor GC中的实际应用
一次Minor GC需要找出所有可能指向新生代的跨代引用,作为GC Roots的一部分。此时流程如下:
- 遍历卡表,收集所有值为非0的卡表项索引
- 根据索引反推出对应的卡页起始地址(如 index × 512)
- 对该卡页内所有对象逐个扫描,识别出指向新生代的引用(如oop指针是否落在年轻代地址范围内)
- 将这些引用加入GC Roots,参与可达性分析
注意点与常见误区
卡表不是万能的,其精度是“粗粒度”的——一个卡页只要有一个跨代引用就会被整体标记,导致可能扫描到大量无关对象。但相比全老年代扫描,性能仍大幅提升。
- 卡表本身占用内存:假设老年代1GB,则卡表约需2MB(1024×1024×1024 / 512 = 2M字节)
- 并发写场景下需保证卡表更新的原子性,HotSpot通过汇编指令(如x86下的lock xadd)实现
- 部分GC算法(如G1、ZGC)不再使用传统卡表,而是采用更精细的Remembered Set或染色指针等机制
卡表本质是以少量空间和写时开销,换取GC时扫描范围的指数级缩减。理解它,有助于读懂GC日志中的“card table scanning”阶段,也能更好评估跨代引用频繁场景下的GC表现。










