Skip to content

P49 面试重灾区:JVM 跨代引用与卡表 ​

面试题:分代 GC 里老年代对象引用了新生代对象,怎么解决跨代引用问题?卡表(Card Table)是什么?

1. 问题背景 ​

GC 找存活对象要从 GC Roots 出发遍历。新生代 GC(Minor GC)只回收新生代,但老年代对象可能引用着新生代对象:

  • 如果不处理:老年代对象就是"GC Roots"(类似),需要扫描整个老年代,Minor GC 就退化成了 Full GC,性能灾难;
  • 如果完全不扫:可能漏掉存活对象,误回收。

2. 解决方案:卡表(Card Table)+ 写屏障 ​

卡表是什么 ​

把老年代内存按 512 字节分块,每块对应卡表(Card Table)里的 1 个字节(bit 位):

text
老年代 512 字节块 → 卡表 1 字节(0=干净,1=脏)

工作过程 ​

  1. 写屏障:老年代对象引用发生变化(写入指向新生代的引用)时,JVM 通过写屏障标记对应卡为脏(1);
  2. Minor GC 时:只扫描脏卡对应的老年代区域(而不是整个老年代),找出其中指向新生代的引用作为 GC Roots 的补充;
  3. 扫描完清除脏标记。

效果:把"扫描整个老年代"变成"扫描被修改过的少量卡",成本大幅降低。

3. 关键点 ​

  • 卡表本质 = 老年代引用新生代的"脏标记位图";
  • 写屏障在引用赋值时触发(不是对象字段赋值都触发,是引用类型赋值);
  • 卡表只有"脏/不脏",不知道具体引用位置,GC 时还是要扫脏卡区域内的对象(粒度是 512 字节块)。

4. 其他收集器的类似机制 ​

  • G1:用 RSet(Remembered Set) 记录"谁引用了本 Region",比卡表更精细(Region 级),但内存开销更大;
  • ZGC:用"染色指针 + 读屏障"等机制,基本没有传统意义上的跨代扫描问题(不分代/染色指针标记)。

5. 高频追问 ​

  • "为什么不全量扫描老年代":老年代可能很大,扫描成本高,卡表用空间(卡表本身很小)换时间;
  • "卡表会不会漏标记":写屏障保证引用变化必被标记;假共享(不同线程同时写相邻卡)会有伪共享问题,可对齐/填充优化;
  • "脏卡什么时候清":Minor GC 扫描处理后清除。

一句话总结 ​

老年代引用新生代若全量扫描代价太大,JVM 用卡表(按 512B 分块的脏标记位图)+ 写屏障:引用变化时标记脏卡,Minor GC 只扫脏卡区域找跨代引用;G1 用更精细的 RSet 做同样的事。

基于 VitePress 重建