rset是每个region独立的反向引用哈希表,存于堆外internal区,条目过多或region过小会导致内存暴涨和扫描退化;卡表为全局byte[]数组,仅作脏卡粗筛,开销线性增长但远小于rset。

跨 Region 引用本身不直接消耗堆内存,但支撑它的卡表(Card Table)和记忆集(RSet)会带来明确的物理开销——不是“有没有”,而是“在哪占、怎么涨、何时压不住”。关键要分清两者的角色和内存归属。
卡表:轻量但全局,吃的是堆外内存中的 Internal 区域
卡表是一张 byte[] 数组,按 512 字节为单位划分整个老年代(及部分大对象区),每个元素标记对应卡页是否“脏”。它本身结构简单,但规模随堆增长线性上升:
- 1GB 堆 → 约 209 万张卡 → 卡表约占用 2MB 堆外内存(每个卡 1 字节)
- 32GB 堆 → 卡表超 64MB,全存于 VM 的 Native Memory 中,归类在 Internal 统计项下
- 它不区分 Region,也不存储引用关系,只做“粗筛”:告诉 Refine 线程“这张卡可能有跨区写入,值得扫”
RSet:真正吃内存的主力,且完全堆外、不可回收
RSet 是每个 Region 独立维护的反向引用哈希表,内容是“谁(Region + 卡号)引用了我”。它的内存开销远高于卡表,且无法被 GC 管理:
- 每条 RSet 条目平均占 24–32 字节(含哈希桶、指针、位图等元数据)
- 一个中等活跃的老年代 Region,RSet 条目常达数万;若被数百个 Region 同时引用(如缓存节点),轻松突破 10 万 entry → 单 RSet 占用超 2MB 堆外内存
- RSet 实例数 = Region 总数;Region 越小(如设成 512KB),总数翻倍 → RSet 实例爆炸,Internal 内存 RSS 骤升,jcmd VM.native_memory summary 中 Internal 占比可能超 15%
写屏障与 Refine 线程失衡:隐性惩罚比显式内存更致命
卡表和 RSet 的开销不只是静态内存。高频跨 Region 写入(如 oldObj.cacheRef = youngObj)会持续触发 G1PostBarrier:
- 每次写屏障需定位源 Region、计算卡号、标记卡表、排队进 dirty card queue
- 若 G1ConcRefinementThreads 线程数不足或 CPU 繁忙,dirty card 积压 → RSet 更新滞后 → Mixed GC 开始时扫描过期 RSet,漏标存活对象
- 后果不是 OOM,而是被迫扩大根集合、Evacuation 失败重试、甚至触发 Concurrent Mode Failure —— 这些都体现为 GC 时间突增、停顿失控,本质是计算资源被写屏障+Refine 持续抢占
Region 大小选错:把开销从“可接受”推向“不可控”
Region 尺寸是卡表与 RSet 开销的杠杆支点,调错会放大所有惩罚:
- Region 太小(≤1MB):Region 数暴涨 → RSet 实例数激增 → Internal 内存碎片化严重,jstat -gc 中 RS 列长期 >15% 堆大小,但堆内对象统计完全看不到这部分
- Region 太大(≥16MB):单 RSet 哈希表桶链过长 → 插入/查询退化为 O(n),写屏障延迟升高;同时易触发 Humongous 分配,绕过 RSet,导致跨代引用漏检
- 实测推荐:堆 ≥32GB 时,-XX:G1HeapRegionSize=8M 是平衡起点;若业务强网状引用(如风控图谱),可试 4M 提升隔离粒度,但需同步监控 Internal 内存增长











