region 是 g1 实现可预测停顿、rset 跨代引用管理及 humongous 对象处理的物理基础:将堆划分为固定大小(2 的幂)的离散 region,使 gc 可按需选择 cset、精准维护 rset 映射,并专设 humongous region 存储大对象。

为了把停顿时间切碎、可预测,而不是攒着一次性爆掉
Region 是 G1 控制 GC 停顿的最小调度单位
传统收集器(比如 ParallelOld 或 CMS)的老年代是一整块连续内存,一旦触发老年代回收,就得扫描、标记、清理甚至压缩整个空间——堆越大,停顿越不可控。G1 把堆切成固定大小的 Region(默认 1~32MB,2 的幂),本质是把“全局任务”拆成“可计量的小任务”。每次 GC 只挑若干个 Region 组成 CSet(Collection Set),回收成本只跟这些 Region 里存活对象的数量有关,和整个堆大小无关。
这直接支撑了 -XX:MaxGCPauseMillis 参数的实际效果:G1 不是靠“猜”,而是基于历史回收数据,估算“回收 N 个 Region 预计耗时 X 毫秒”,再反推本次该选多少个 Region。
- Region 大小必须是 2 的幂,否则 JVM 启动会报错:
Invalid value for -XX:G1HeapRegionSize - Region 数量默认目标是约 2048 个,JVM 用
heap_size / 2048向上取整到最近的 2 的幂来算大小;比如 8GB 堆 → 约 4MB/Region - 太小的 Region(如 1MB)会导致 RSet 开销剧增;太大的 Region(如 32MB)会让单次回收波动变大,削弱停顿控制能力
Region 让跨代引用扫描不再需要全堆遍历
年轻代 GC 时,必须知道哪些老年代对象引用了年轻代对象,否则会漏掉存活对象。CMS 用卡表(Card Table)+ 写屏障粗粒度标记,但仍有漏标风险;G1 改用 Remembered Set(RSet),每个 Region 自带一个 RSet,只记录“哪些其他 Region 里的哪些卡(Card,512 字节)有指向我的引用”。
这个结构能成立,前提是内存被划分为离散、边界对齐的 Region——只有这样,才能精确映射“某段地址属于哪个 Region”,进而定位到对应 RSet。
- 写屏障在每次
obj.field = otherObj时触发,异步更新目标 Region 的 RSet - Young GC 时,只需扫描 Eden/Survivor Region + 它们 RSet 中标记的跨区引用,完全跳过未被引用的老年代 Region
- 如果没有 Region 划分,RSet 就无法建立“区域→引用源”的映射关系,这套机制就垮了
Region 支撑 Humongous 对象的特殊分配与回收
G1 把 ≥ 当前 Region 大小一半的对象视为巨型对象(Humongous Object),比如 Region 是 2MB,那 ≥1MB 的数组就走 Humongous 分配路径。这类对象不进 Eden,而是直接占用连续的 Humongous Region,且每个 Humongous Region 只存一个对象。
这种策略只有在 Region 划分下才可行:它避免了大对象在普通 Region 中造成内部碎片,也使得回收时可以整块释放——只要该 Humongous Region 里没其他引用,下次 GC 就能直接回收,不用复制。
- Humongous 对象不会被 Young GC 触发晋升,也不会参与 Mixed GC 的常规 CSet 选择,而是单独在 Full GC 或下一次并发标记后被识别为可回收
- 如果堆里大量存在略小于 Region 一半的对象(比如 Region=4MB,对象=1.9MB),它们会被塞进普通 Region 导致严重内部碎片,反而降低吞吐——这是调优时最容易忽略的隐形坑
-
-XX:G1HeapRegionSize设得过大(如 32MB),会让本该是 Humongous 的对象变少,但同时导致年轻代 Region 数量锐减,Young GC 频率上升
Region 不是为“看起来更现代”而设计的抽象,它是 G1 所有关键能力的物理前提:停顿可控、RSet 可建、Humongous 可管、Mixed GC 可行。一旦脱离 Region,G1 就退化回传统分代模型——这也是为什么你改了 -XX:G1HeapRegionSize 却发现停顿没改善,大概率是 Region 大小和实际对象分布没对齐。真正难的从来不是参数本身,而是理解每个 Region 在那一刻承担的角色:是 Eden?是 Old?还是正被 RSet 引用着的 Humongous 载体?











