G1 垃圾回收器深度解析
G1(Garbage First)从 JDK 9 起成为默认垃圾回收器。它把堆划分为大量 Region,实现了"可预测的停顿时间模型",兼顾吞吐与延迟,是对经典分代收集的一次架构级重写。
Region 分区设计
G1 不再区分物理上的新生代/老年代,而是把整个堆划分为 2048 个左右大小相等的 Region(默认 1MB ~ 32MB,2 的幂次),每个 Region 可动态扮演不同角色:
┌────────────────────────────────────────────────────┐
│ E │ E │ O │ S │ H │ E │ E │ O │ O │ S │
│ E │ O │ O │ E │ H │ E │ O │ E │ O │ O │
│ S │ O │ E │ E │ H │ O │ O │ E │ S │ E │
└────────────────────────────────────────────────────┘
E = Eden S = Survivor O = Old H = Humongous(巨型对象)- Eden/Survivor:新生代 Region,逻辑上构成新生代
- Old:老年代 Region
- Humongous:超过 Region 一半大小的对象,连续占用多个 Region
Region 化的核心收益:回收单位从"整个代"细化为"单个 Region",GC 可以只回收价值最高的 Region("Garbage First" 的由来)。
可预测停顿模型
G1 维护一个 停顿预测模型,记录每个 Region 的回收耗时与收益,按"回收价值 = 回收内存 / 回收耗时"排序。
-XX:MaxGCPauseMillis=200(默认) ← 停顿软目标G1 根据预测,在停顿预算内选择性价比最高的 Region 集合回收,尽量使每次 GC 停顿不超过设定值。注意这是软目标,并非绝对保证。
跨区引用与 Remembered Set
Region 之间互相引用(老年代 Region 引用新生代 Region),G1 不能每次 GC 都全堆扫描。为此每个 Region 维护一个 RSet(Remembered Set),记录"谁引用了本 Region 的对象"。
Region A 的对象被 Region B 引用
→ 在 Region A 的 RSet 中记录:Region B
GC 回收 Region A 时,只需扫描 RSet 记录的 Region,而非全堆RSet 的维护依赖写屏障:对象引用被写入时,通过写后屏障把引用关系记录到对应 Region 的 RSet 中。
并发标记:SATB
G1 的并发标记采用原始快照(SATB),快照标记开始时堆中所有存活对象,防止并发期间引用被修改导致漏标。
初始标记(STW):标记 GC Roots 直接可达对象,与 Minor GC 合并执行
并发标记:与用户线程并发遍历对象图
最终标记(STW):处理 SATB 队列中记录的引用变更
清理(STW 部分):统计各 Region 存活对象,供后续选择回收 Region与 CMS 的增量更新不同,SATB 记录的是"删除的引用",代价是可能保留本轮可回收的浮动垃圾,但实现简单且不会漏标。
GC 模式与 Mixed GC
G1 的 GC 分为三种:
Young GC
新生代 Region 占满时触发,复制算法回收 Eden + Survivor 中的存活对象。
- 只回收年轻代 Region,停顿短
- 老年代 Region 通过 RSet 参与扫描,但不回收
Mixed GC
当老年代占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%)时,G1 启动并发标记,标记完成后触发 Mixed GC:回收全部新生代 Region + 部分高价值老年代 Region。
Mixed GC 回收集合 = 所有 Eden/Survivor Region + 性价比最高的 Old RegionFull GC
并发收集失败(如对象分配速度超过回收速度)时退化为 Full GC:
- 单线程、STW、标记-整理全堆
- 是 G1 最坏的停顿场景,需要靠调优避免
| 阶段 | 回收对象 | 特点 |
|---|---|---|
| Young GC | 新生代 Region | 频繁、停顿短 |
| Mixed GC | 新生代 + 部分老年代 | 周期性清理老年代 |
| Full GC | 全堆 | 兜底、停顿长 |
调优参数
| 参数 | 作用 | 默认 |
|---|---|---|
-XX:+UseG1GC | 启用 G1 | JDK 9+ 默认 |
-XX:MaxGCPauseMillis | 停顿软目标 | 200ms |
-XX:G1HeapRegionSize | Region 大小 | 1MB~32MB 自动 |
-XX:InitiatingHeapOccupancyPercent | 触发并发标记的堆占用率 | 45% |
-XX:G1NewSizePercent / -XX:G1MaxNewSizePercent | 新生代占比范围 | 5% / 60% |
-XX:ConcGCThreads | 并发标记线程数 | CPU 的 1/4 |
-XX:G1MixedGCCountTarget | Mixed GC 分批次数 | 8 |
-XX:MaxGCPauseMillis 调小 | 更多 Young GC、更短停顿 | 200ms |
调优实践思路
- 先跑通再调优:优先保证应用功能正确,收集 GC 日志观察实际行为
- 停顿过长:调大 Region 数量(减小 Region 大小)、调小
MaxGCPauseMillis、检查是否触发 Full GC - 频繁 Full GC:多为对象分配过快或晋升过多,检查
IHOP是否过小、大对象(Humongous)是否过多 - 大对象问题:巨型对象直接占用连续 Region,易引发过早并发标记,可考虑
-XX:G1HeapRegionSize调大以容纳大对象 - 不要盲目调参:G1 的参数敏感,先观察日志与 GC 频率再动手
常见问题
- G1 与 CMS 的区别? 结构上 G1 用 Region 替代物理分代,回收单位更细;标记方案上 G1 用 SATB,CMS 用增量更新;G1 有停顿预测模型。
- G1 会触发 Full GC 吗? 会。并发回收速度跟不上分配速度、或巨型对象分配失败时,G1 退化为 Full GC(STW 全堆整理)。
- 什么时候不用 G1? 超大堆(百 GB 级)追求极低延迟可考虑 ZGC;JDK 8 老项目升级前需验证兼容性。