G1(Garbage-First)是 Java HotSpot 虚拟机自 JDK 7u4 起引入并在 JDK 9 中成为默认的服务器端垃圾回收器。它的核心设计目标是替代老旧的 CMS(Concurrent Mark Sweep),在大堆内存(>4GB ~ 几十GB甚至上百GB)的生产环境下,兼顾高吞吐量软实时、可预测的低延迟(Soft Real-Time Low Latency)


核心设计哲学与特性

传统的垃圾回收器(如 Serial、Parallel、CMS)在物理上将堆内存严格划分为固定且连续的新生代(Eden、Survivor)与老年代(Old)。而 G1 颠覆了这种传统的物理连续布局:

D2 Diagram
qtopie.github.io

核心特性


核心数据结构与底层机制

Remembered Set(RSet 记忆集)

在跨 Region 分区收集时,若 Region-A 的对象引用了 Region-B 的对象,如何避免为了回收 Region-B 而对整个堆进行全量扫描?G1 引入了 RSet(Remembered Set)

三色标记法与 SATB(Snapshot-At-The-Beginning)

在并发标记(Concurrent Marking)阶段,GC 线程与用户业务线程并发执行。为了保证在对象图动态变化时不发生漏标与误标,G1 结合了三色标记法SATB(原始快照)


G1 垃圾回收全生命周期

G1 的垃圾回收并不是单调重复的简单过程,而是由 Young GC并发标记周期(Concurrent Marking Cycle)Mixed GC 以及作为后备兜底的 Full GC 组成的自适应闭环:

D2 Diagram
qtopie.github.io

Young GC(年轻代收集)

并发标记周期(Concurrent Marking Cycle)

当老年代空间占用达到堆总容量的 -XX:InitiatingHeapOccupancyPercent(默认 $45%$)时启动,包含五个阶段:

  1. 初始标记(Initial Mark, STW): 标记 GC Roots 能直接关联到的对象。通常借道(Piggybacked)在一次普通的 Young GC 中同步完成,借用 Young GC 的 STW 停顿,基本不增加额外延迟。
  2. 根区域扫描(Root Region Scanning, 并发): 扫描由初始标记中 Survivor 区对象引用的老年代对象。必须在下一次 Young GC 触发前完成。
  3. 并发标记(Concurrent Mark, 并发): 遍历整个堆的对象图,进行全局三色标记,同时处理 SATB 写前屏障记录的引用快照。
  4. 最终标记 / 重新标记(Remark, STW): 短暂 STW,清空并发阶段剩余的 SATB 缓冲区,完成最后漏标对象的修正。
  5. 清理阶段(Cleanup, 部分 STW):
    • 统计每个 Region 的存活对象比例与回收价值;
    • 完全空闲(没有存活对象)的老年代 Region 会在此时直接释放并重置,无需经过后续的复制拷贝;
    • 准备可回收集合(CSet),为后续 Mixed GC 铺路。

Mixed GC(混合收集)

Full GC(兜底衰退)

当对象晋升速度远超过并发标记与 Evacuation 释放速度时,老年代空间被快速填满,或者没有足够连续的 Region 容纳大对象,便会触发 Evacuation Failure(To-space Exhausted),导致 G1 退化为 Full GC。


停顿时间控制原理(MaxGCPauseMillis)

用户通过 -XX:MaxGCPauseMillis=200(默认 200ms)设置最大停顿目标。G1 是如何做到尽量满足该目标的?

  1. 可预测耗时衰减模型: G1 内部记录了每次 GC 中各个阶段的历史耗时(如扫描一张卡表平均耗时、复制一个对象字节的平均耗时、更新 RSet 耗时)。通过历史数据的指数衰减加权均值(Decay Average),建立起严格的“耗时预测数学模型”。
  2. CSet(Collection Set,回收集)动态剪裁: 在决定本次 GC 收集哪些 Region 时: $$\text{预估停顿时间} = \text{年轻代全部 Region 预估复制时间} + \sum \text{选中的老年代 Region 预估复制时间} + \text{固定基础设施耗时}$$ 如果预估时间快达到 MaxGCPauseMillis,G1 会动态减少加入 CSet 的老年代 Region 数量,甚至动态收缩年轻代大小,从而保证停顿时间严格不超标。
  3. 调优认知误区:

    [!IMPORTANT] MaxGCPauseMillis 只是一个软目标(Soft Target),并非硬性 SLA 保证。如果盲目将该值设得过低(例如 30ms),G1 为了达标会拼命缩减年轻代 Region 数量,导致年轻代极小,从而引发高频 Young GC,甚至导致对象快速溢出进入老年代而频繁触发 Full GC。通常推荐设置为 $100\text{ms} \sim 300\text{ms}$。


生产常见问题:Full GC 频繁原因与完整排查

Full GC(整堆与元空间回收)频繁触发的本质,是对象未能被年轻代有效过滤而过快进入老年代(或老年代空间耗尽),以及元空间、堆外内存不足或显式触发。在 G1 中,Full GC 会导致不可接受的全局长停顿,是系统可用性的重大隐患。

常见根因分类与防御

  1. 大对象(Humongous Object)分配过多或无序

    • 机制: 对象大小超过单个 Region 50% 的对象会被判定为巨型对象,直接分配在连续的巨型 Region(属于老年代)。
    • 典型场景: 未分页的超大 SQL 查询(如一次性查询几十万记录)、大文件或报表(Excel/CSV)非流式导出、超大 JSON/ProtoBuf 序列化。
    • 后果: 巨型对象难以找到物理连续的 Region 块,直接导致内存碎片化加剧,极易触发 Full GC。
    • 解法: 调大 Region 尺寸(-XX:G1HeapRegionSize=16m32m);代码重构为流式处理(如 EasyExcel、MyBatis 流式游标)。
  2. 内存泄漏(Memory Leak)

    • 典型场景: 静态集合(static Map/List)无上限缓存数据;ThreadLocal 线程池复用未在 finally 中显式 remove();未关闭的连接与监听器。
    • 后果: 存活对象随时间推移全部堆积在老年代,每次 Full GC 回收收益几乎为 0(老年代水位居高不下)。
    • 解法: 导出堆转储(Heap Dump),使用 MAT (Memory Analyzer Tool) 查看 Leak Suspects 报表与 GC Roots 强引用链。
  3. 对象过早晋升(Premature Promotion / Promotion Failure)

    • 机制诱因:
      • 动态年龄判定(Dynamic Tenuring): Survivor 区中相同年龄所有对象大小总和超过 Survivor 的 50%(-XX:TargetSurvivorRatio)时,年龄大于或等于该年龄的对象直接提前晋升老年代,无需达到 -XX:MaxTenuringThreshold
      • 突发流量与慢请求: 突发高并发下产生大量生命周期略长的临时对象(如耗时 23 秒的复杂 RPC/DB 调用上下文),对象跨越了 12 次 Minor GC,Eden 迅速填满且 Survivor 承载不下,触发分配担保提前甩入老年代。
    • 应对策略(三层防御):
      • 业务与代码层(根治): 缩短请求链路 RT(优化慢 SQL、加缓存、异步并发调用),让请求上下文对象在 Eden 区快速消亡;瘦身 DTO/Context,避免大宽表实体进内存;评估高频字节数组或大缓冲区池化复用。
      • JVM 参数调优(缓解):
        • 针对 G1: 调高年轻代最小占比 -XX:G1NewSizePercent=20~30(默认 5%),预留弹性缓冲;适当放宽停顿目标 -XX:MaxGCPauseMillis=200(避免停顿设太小导致 G1 主动压缩年轻代 Region 逼出过早晋升);调大 Region 尺寸 -XX:G1HeapRegionSize=16m/32m 防 Humongous 判定;下调 -XX:InitiatingHeapOccupancyPercent=40 提前启动 Mixed GC。
        • 针对传统分代(CMS/Parallel): 调大年轻代 -Xmn 或减小 -XX:NewRatio=1;减小 -XX:SurvivorRatio=46 扩大 Survivor 物理容量;提高动态判定阈值 -XX:TargetSurvivorRatio=80;提高最大晋升年龄 -XX:MaxTenuringThreshold=15
      • 架构与网关层(兜底): 网关(Sentinel/Kong)配置并发线程数与令牌桶限流削峰;非实时/重批处理任务通过 MQ(RocketMQ/Kafka)异步削峰填谷;K8s 容器层配置基于 CPU/QPS 指标的 HPA 弹性伸缩。
  4. 并发模式失败(Concurrent Mode Failure / Evacuation Failure)

    • 如果对象分配速率远快于 G1 的并发标记清理速度,或者 Mixed GC 来不及腾出足够的可用 Region,当发生晋升失败(To-space Exhausted)时,G1 会被迫退化为全堆 STW 的 Full GC。
    • 参数关联: -XX:InitiatingHeapOccupancyPercent (IHOP) 设置过高,导致并发标记启动过晚。建议下调为 35% ~ 40%
  5. 元空间(Metaspace)满导致 Full GC

    • 大量使用 CGLIB、动态代理、反射、Groovy 脚本动态编译加载类,导致类加载器及方法区元数据不断膨胀。
    • 当元空间使用量达到阈值(-XX:MetaspaceSize)且需要扩张时,JVM 会强制触发一次 Full GC 尝试卸载无用的 ClassLoader。
    • GC 日志特征:包含 Metadata GC Threshold。建议显式配置足够大的元空间阈值,如 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
  6. 代码显式调用 System.gc() 或堆外直接内存不足

    • 业务代码或早期第三方库隐式调用 System.gc()(日志中含 System.gc())。配置 -XX:+DisableExplicitGC 彻底禁用。
    • Netty / NIO 频繁申请 DirectByteBuffer 且未及时释放,堆外直接内存不足时触发 Bits.reserveMemory() 内部调用 System.gc() 回收堆外资源。

排查与定位链路

1. GC 日志与监控定位
   ├── 查看触发原因:Allocation Failure / System.gc() / Metadata GC Threshold / To-space Exhausted
   └── 观察老年代水位变化:回收后水位骤降(大对象/瞬时突发)还是居高不下(内存泄漏)

2. 线上实时排查命令
   ├── jstat -gcutil <pid> 1000 10    (查看 Eden/Survivor/Old/Metaspace 使用率及 YGC/FGC 次数与耗时)
   ├── jmap -histo:live <pid> | head -n 20 (查看存活对象排行榜,快速锁定 byte[]、String 或高危业务实体)
   └── jcmd <pid> GC.class_histogram

3. 离线深度定位(定位代码行)
   ├── 导出堆转储:jmap -dump:format=b,file=heap.hprof <pid>(生产注意先切走流量)
   └── 工具分析:使用 Eclipse MAT 或 JProfiler 查看 Leak Suspects、GC Roots 引用链与支配树 (Dominator Tree)

G1 vs CMS 核心对比与选型

比较维度CMS 垃圾回收器G1 垃圾回收器
内存物理模型固定且连续的物理分代(Eden/Survivor/Tenured)离散的 Region 分区(每个 Region 逻辑扮演不同角色)
回收算法老年代使用标记-清除(Mark-Sweep),新生代使用复制老年代使用标记-整理/复制(Copying),全局无离散碎片
STW 停顿控制无法设置目标,停顿时间不可预测支持 -XX:MaxGCPauseMillis 软实时目标与预测模型
大对象处理直接进入连续的老年代空间专门的连续 Humongous Region,针对性收集
并发漏标处理增量更新(Incremental Update),破坏黑色到白色引用原始快照(SATB),破坏灰色到白色引用断开,Remark 更快
额外内存开销仅依赖卡表(Card Table),内存开销小(约 $1%$ 堆大小)每个 Region 维护独立 RSet,内存额外开销大($5% \sim 15%$)
适用堆内存范围适合小堆与中等堆($< 4\text{GB} \sim 6\text{GB}$)适合大堆($\ge 6\text{GB} \sim$ 上百 GB)

生产推荐配置与调优指南

在生产环境中,使用 G1 遵循 “尽量减少手动干预,信任 G1 自适应机制” 的原则,通常只需配置以下核心参数:

# 1. 基础堆与垃圾回收器指定
-XX:+UseG1GC
-Xms8g -Xmx8g

# 2. 停顿时间目标(最核心参数,切勿设置过于极端)
-XX:MaxGCPauseMillis=200

# 3. 避免 Humongous 碎片与提升单 Region 容量(8G+ 堆推荐 4MB~16MB)
-XX:G1HeapRegionSize=8m

# 4. 解决突发流量过早晋升:调大年轻代保底比例(默认 5% 容易被突发打爆)
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=60

# 5. 提前启动老年代并发标记,预防 Evacuation Failure(默认 45%)
-XX:InitiatingHeapOccupancyPercent=40

# 6. 禁用显式 System.gc()
-XX:+DisableExplicitGC

# 7. GC 日志配置(JDK 8 语法)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/app/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=100M
# 注:若在 JDK 9+ 请采用 Unified JVM Logging:-Xlog:gc*,gc+phases=debug:file=/var/log/app/gc.log:time,uptime,pid:filecount=5,filesize=100M

参考与扩展