Card Table是JVM为解决Minor GC时跨代引用漏标问题而设计的优化机制:将老年代划分为512字节的卡页,用字节数组标记“可能含老→年引用”的脏卡,仅扫描脏卡即可避免全老年代遍历。

Card Table 是什么,它解决什么问题
Minor GC 时不能只扫描年轻代,因为老年代对象可能引用着年轻代里的存活对象。如果忽略这些跨代引用,被引用的对象会被误判为垃圾并回收,程序直接崩溃。全堆扫描老年代又太慢——Card Table 就是这个矛盾的折中解:它不记录具体哪个老年代对象引用了哪个年轻代对象,而是把老年代按固定大小(通常 512 字节)划分为“卡页(Card)”,用一个字节数组标记哪些卡页“可能含有跨代引用”。标记为 dirty 的卡页,才需要在 Minor GC 时被扫描。
写屏障如何触发 Card Table 更新
每次老年代对象执行类似 obj.field = youngObj 这样的赋值操作时,JVM 不会等 GC 来临时再补救,而是在写入瞬间通过写屏障(Write Barrier)拦截该操作,并计算出 youngObj 所在卡页的索引,将对应位置的 card table 数组元素设为 dirty。这个过程开销极小,但能保证“引用变更”不会漏记。
常见误区:
- 写屏障只对“老→年”引用生效;
young → young或old → old不触发 - 不是所有 GC 算法都用 Card Table:Serial/Parallel 收集器用的是更粗粒度的
remembered set变体;G1 和 ZGC 则用更精细的Remembered Set(RSet)替代了传统 Card Table - Card Table 本身不存引用关系,只是“脏卡”开关;真正构建跨代引用映射靠后台
Refinement Thread消费脏卡队列后扫描生成 RSet
为什么 Card Table 查找比全堆扫描快得多
假设老年代 2GB,按 512 字节分卡,总共约 4M 个卡页;其中真正 dirty 的通常不到 0.1%。Minor GC 时只需遍历这几千个脏卡页,对每个卡页做一次粗略扫描(比如检查卡内所有对象头是否指向年轻代),就能收集成千上万个跨代引用点。相比遍历全部 2GB 老年代内存,耗时从秒级降到毫秒级。
关键参数影响:
-
-XX:+UseCondCardMark:启用条件式卡表写入,避免重复标记同一卡页,减少写屏障开销 -
-XX:CardTableStyle=1(HotSpot 内部):控制卡表实现方式,普通应用无需调整 - 卡页大小不可配置,硬编码为 512 字节(
CardTable::card_size),改不了
调试和验证 Card Table 是否生效
不能直接打印 Card Table 内容,但可通过 GC 日志间接观察效果:
- 开启
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,对比开启/关闭-XX:+UseConcMarkSweepGC(CMS)或-XX:+UseG1GC时 Minor GC 的暂停时间变化 - 用
jstat -gc <pid></pid>观察YGCT(Young GC 时间)是否稳定;若某次 Minor GC 时间突增,可能是大量脏卡导致 RSet 构建压力上升 - 触发
java -XX:+PrintAdaptiveSizePolicy可看到 JVM 自动调整 Survivor 区比例,背后就依赖 Card Table 提供的老年代引用热度反馈
真正容易被忽略的是:Card Table 只保障“不漏标”,不保障“不误标”。它标记的是“可能有跨代引用”的卡页,所以 Minor GC 扫描的仍是保守集合——这也是为什么 RSet 在 G1 中被拆成 per-region 的细粒度结构,进一步压缩扫描范围。

















