卡表仅服务于minor gc,不参与老年代gc;它通过标记老年代中可能含跨代引用的512字节卡页,缩小年轻代回收时的老年代扫描范围,但对major gc或full gc无加速作用。

卡表(Card Table)本身不参与老年代垃圾回收,它只服务于年轻代回收(Minor GC),对老年代回收(Major GC / Full GC)没有加速作用。 这是绝大多数人误用或误调优的起点——试图靠调整卡表相关参数来降低老年代停顿,结果毫无效果。
为什么 Card Table 对老年代 GC 完全无效
Card Table 的设计目标非常明确:仅用来 缩小 Minor GC 扫描老年代的范围。它记录的是「老年代中哪些 512 字节内存块(card)可能含有指向年轻代的引用」。而老年代 GC(比如 CMS、Parallel Old、G1 Mixed GC 中的老年代部分)需要扫描的是「本代内对象的存活关系」,和跨代引用无关。
- Card Table 不存任何老年代内部引用信息,也不参与老年代标记阶段
- 老年代 GC 时,JVM 会直接遍历整个老年代(或其 Region/分区),不会查 Card Table
- 写屏障(Write Barrier)只在
old → young赋值时触发卡表更新;old → old写入完全不碰卡表 - 参数如
-XX:+UseCondCardMark或-XX:CardTableStyle只影响卡表置脏效率,不影响老年代扫描逻辑
真正影响老年代停顿的关键机制是 Remembered Set(RSet)
很多人混淆 Card Table 和 RSet。Card Table 是“线索生成器”,RSet 才是“线索汇总表”。但注意:RSet 主要用于 G1 和 ZGC 等分区式收集器,不是所有 GC 都有 RSet。Parallel Old、CMS 等传统分代收集器根本不维护 RSet。
- G1 的每个 Region 都有自己的 RSet,记录其他 Region 指向它的引用;Mixed GC 时只扫描 dirty RSet,跳过无引用的 Region
- RSet 的构建依赖 Card Table + 后台 Refinement 线程,但一旦构建完成,Card Table 就退场了
- 如果老年代停顿高,优先检查
G1RemSetProcessingPhaseTime和G1RefinePhaseTime(用-XX:+PrintGCDetails输出),而不是卡表 - 盲目增大
-XX:G1ConcRefinementThreads可能导致 CPU 争抢,反而延长 STW
优化老年代停顿该看什么,而不是卡表
卡表无法调,但老年代停顿可调。关键不在“怎么让卡表更快”,而在“怎么减少老年代必须处理的工作量”。
- 控制晋升:用
-XX:MaxTenuringThreshold和-XX:TargetSurvivorRatio避免短命对象过早进老年代 - 限制老年代增长:通过
-XX:NewRatio或-Xmn给年轻代留足空间,减少 Minor GC 频次,也就减少晋升压力 - 避免大对象直接进老年代:监控
Humongous Allocation日志,用-XX:G1HeapRegionSize调整 Region 大小,防止小对象被误判为大对象 - 慎用
System.gc():它强制触发 Full GC,绕过所有分代优化,卡表、RSet 全部失效
卡表是 JVM 内部一个极其精巧的“减法工具”——它让你不用扫整个老年代,但它本身不是“老年代的索引”。想压低老年代停顿,得回到对象生命周期本身:谁该待在老年代?谁不该去?去了之后多久该走?这些才是真实可控的杠杆。卡表只是安静地站在后台,把 old → young 的每一次赋值翻译成一个 byte 的 0x00 → 0xff。










