调优G1的RSet核心目标是控制跨Region引用的记录与扫描精准轻量,避免成为GC瓶颈;需匹配业务对象粒度设RegionSize(如缓存用4M/8M),监控RSet内存占比(≤1.5%)和Mixed GC扫描耗时,同步调优G1ConcRefinementThreads等参数。

调优 G1 的 Remembered Set(RSet)核心目标很明确:让跨 Region 引用的记录和扫描既精准又轻量,避免它成为 GC 停顿或内存占用的瓶颈。关键不在“压低 RSet 大小”,而在于控制谁在引用、怎么引用、在哪引用。
RegionSize 必须匹配业务对象粒度
Region 太小,RSet 实例爆炸;太大,单个 RSet 查找变慢、还容易催生 Humongous 对象绕过 RSet。这不是拍脑袋定的参数:
- 缓存类应用常见 4–8MB 商品快照或序列化缓冲区 → 设 -XX:G1HeapRegionSize=4M 或 8M(必须是 2 的幂)
- 堆 ≥64GB 时,默认推导常偏小(如 1MB),易致 RSet 元数据吃掉大量堆外内存 → 手动设为 8M 起步,再观察 GC 日志中 [RSet: XMB] 占比
- 若 RSet memory usage > 15%(jstat 或 NMT 可见),优先怀疑 RegionSize 过小;若 Mixed GC 阶段 [scan: Zms] > 20ms,且单个 RSet entries 超 10 万,可能是 RegionSize 过大导致哈希冲突
收拢引用关系,减少跨 Region 写入
RSet 膨胀只跟“被多少 Region 引用”有关,跟本 Region 有多少对象无关。高频跨 Region 赋值(比如老对象反复更新指向新生代缓存 value)会持续触发 write barrier,拖慢 mutator 线程并堆积 dirty card:
- 把强关联数据(如 key + value + TTL 字段)尽量分配在同一 Region → 用对象池复用缓冲区,或批量构建后一次性分配
- 避免全局静态容器(如 ConcurrentHashMap
)长期持有短生命周期对象;改用弱引用、软引用,或引入显式失效机制 - 对长生命周期配置类,拆分为更细粒度 Region 分配,而非一个巨型对象塞满整个 Region
盯紧 RSet 更新链路与混合回收阶段
RSet 不是实时同步的,它依赖 G1ConcRefinementThreads 从脏卡队列异步构建。一旦写入速度超过 Refine 吞吐,Mixed GC 就可能漏标:
- 用 jstat -gc
观察 CCSU(Concurrent RS Update)时间占比,持续 >10% 就说明 Refine 线程跟不上 - 加参数 -XX:G1ConcRefinementThreads=4(默认通常为 2 或 3),尤其在多核高吞吐服务上
- Mixed GC 阶段才是 RSet 扫描主战场,关注日志中 [RSet: XMB, Y entries][scan: Zms] 组合;Young GC 的 RSet 开销通常可忽略
- 若频繁出现 Concurrent mode failure 或 Evacuation 时间突增,先查 RSet 更新是否滞后,而不是盲目调大堆或改 Mixed GC 次数
诊断要靠日志交叉验证,别信直觉
GC 日志里看不到“卡表命中数”,因为 G1 已弃用传统 Card Table。真正有效的是:
- JDK 10+:-Xlog:gc+remset*=debug:file=gc-rset.log:time,tags,level
- JDK 8u262+:-XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy(看 RSet 条目与扫描耗时)
- 配合 jcmd
VM.native_memory summary 查 Internal 区域突增,确认是否 RSet 吃掉堆外内存 - 重点不是 RSet 有多大,而是它是否导致 scan 耗时上升 或 CCSU 持续高位 —— 这才是性能拐点









