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=脏)工作过程
- 写屏障:老年代对象引用发生变化(写入指向新生代的引用)时,JVM 通过写屏障标记对应卡为脏(1);
- Minor GC 时:只扫描脏卡对应的老年代区域(而不是整个老年代),找出其中指向新生代的引用作为 GC Roots 的补充;
- 扫描完清除脏标记。
效果:把"扫描整个老年代"变成"扫描被修改过的少量卡",成本大幅降低。
3. 关键点
- 卡表本质 = 老年代引用新生代的"脏标记位图";
- 写屏障在引用赋值时触发(不是对象字段赋值都触发,是引用类型赋值);
- 卡表只有"脏/不脏",不知道具体引用位置,GC 时还是要扫脏卡区域内的对象(粒度是 512 字节块)。
4. 其他收集器的类似机制
- G1:用 RSet(Remembered Set) 记录"谁引用了本 Region",比卡表更精细(Region 级),但内存开销更大;
- ZGC:用"染色指针 + 读屏障"等机制,基本没有传统意义上的跨代扫描问题(不分代/染色指针标记)。
5. 高频追问
- "为什么不全量扫描老年代":老年代可能很大,扫描成本高,卡表用空间(卡表本身很小)换时间;
- "卡表会不会漏标记":写屏障保证引用变化必被标记;假共享(不同线程同时写相邻卡)会有伪共享问题,可对齐/填充优化;
- "脏卡什么时候清":Minor GC 扫描处理后清除。
一句话总结
老年代引用新生代若全量扫描代价太大,JVM 用卡表(按 512B 分块的脏标记位图)+ 写屏障:引用变化时标记脏卡,Minor GC 只扫脏卡区域找跨代引用;G1 用更精细的 RSet 做同样的事。