news 2026/4/17 0:08:48

【JVM深度解析】第06篇:G1垃圾收集器深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【JVM深度解析】第06篇:G1垃圾收集器深度解析

摘要

G1(Garbage-First)垃圾收集器是 JDK 9+ 的默认 GC,通过将堆划分为等大小的 Region 区域,彻底摒弃了传统的物理连续分代结构,实现了可预测的停顿时间模型。本文深入解析 G1 的核心设计:Region 分区机制与 Humongous 对象处理、四种 GC 类型(Young GC/Mixed GC/Concurrent Cycle/Full GC)的触发条件与执行流程、SATB(Snapshot-At-The-Beginning)写屏障的并发标记方案、RSet(Remembered Set)的跨 Region 引用追踪,以及停顿时间预测模型的实现原理。附完整的 G1 调优参数详解与生产环境最佳实践,帮助开发者充分发挥 G1 的能力。


引言

当你的服务堆内存超过 4GB,Full GC 一次停顿动辄 2-5 秒,CMS 的碎片问题反复导致 Concurrent Mode Failure,是时候拥抱 G1 了。

G1 的设计哲学与传统 GC 截然不同:不追求最低停顿,而是追求可预测的停顿。"可预测"意味着你可以告诉 G1:“每次停顿不超过 200ms”,G1 会尽力实现这个承诺——这是传统 GC 做不到的事情。

G1 自 JDK 6u14 实验性引入,JDK 7u4 正式引入,JDK 9 成为默认收集器。如今大多数 JDK 11/17/21 应用默认就在使用 G1,深入理解它,是现代 Java 性能调优的必修课。


一、G1 的核心设计:Region 分区

1.1 从"连续分代"到"Region 化"

传统 GC 将堆划分为连续的物理区域:

传统堆布局(CMS/Parallel): ┌────────────────────────────────────────────────────────────────┐ │ 年轻代(Young Gen) │ 老年代(Old Gen) │ │ Eden │ S0 │ S1 │ │ └────────────────────────────────────────────────────────────────┘ 物理上必须连续,大小固定(或需要停顿才能调整)

G1 打破了这种束缚:

G1 堆布局: ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │E │O │E │O │E │H │H │H │S │O │E │O │E │S │O │E │ ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ │O │E │S │E │O │E │O │E │O │E │S │O │E │O │E │O │ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ E = Eden Region O = Old Region S = Survivor Region H = Humongous Region(大对象,横跨多个 Region) 空白 = 未使用的 Free Region 特点: - Region 大小统一(1M~32M,必须是2的幂) - 每个 Region 角色可以动态改变 - Region 物理上不连续,逻辑上按代组织 - 默认总 Region 数约 2048 个

1.2 Region 大小计算

Region 大小=堆最大值 /2048(取最近的2的幂) 示例:-Xmx4g→ 4096M/2048=2M 每 Region-Xmx8g→ 8192M/2048=4M 每 Region-Xmx16g→ 16384M/2048=8M 每 Region 手动指定(一般不需要):-XX:G1HeapRegionSize=4m# 指定 Region 大小

1.3 Humongous 对象(大对象处理)

当对象大小≥ Region 大小的 50%时,被视为 Humongous 对象,直接分配到老年代中的专用 Humongous Region:

Region 大小 = 4MB 对象大小 = 3MB(>= 2MB = 50%)→ Humongous 对象 Humongous 分配示意: ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ │H1│H1│ │H2│H2│H2│ │ └──┴──┴──┴──┴──┴──┴──┴──┘ ↑──────┘ ↑──────────┘ 3MB对象占2个Region 7MB对象占3个Region 特点: - Humongous 对象直接进老年代(跳过年轻代) - Humongous Region 不做复制(移动大对象代价太高) - JDK 8u40+ Concurrent Cycle 也能回收 Humongous 对象 - 频繁分配大对象是导致 GC 停顿频繁的常见原因 优化建议: 避免频繁创建短命的大对象(如大 byte[]、大集合类), 或增大 Region 大小以提高 Humongous 阈值

二、G1 的四种 GC 类型

2.1 G1 GC 类型总览

G1 GC 类型与触发关系: 堆内存使用率 │ │ IHOP 阈值(默认45%) │──────────────────→ 触发 Concurrent Marking Cycle(并发标记周期) │ │ Eden 区满 │──────────────────→ 触发 Young GC(纯年轻代 GC) │ │ Mixed GC 触发条件满足 │──────────────────→ 触发 Mixed GC(年轻代 + 部分老年代) │ │ G1 无法满足停顿时间目标 / 并发标记失败 └──────────────────→ Full GC(单线程,退化,应避免)

2.2 Young GC

触发条件:Eden Region 分配满,无法再分配新对象

执行流程

Young GC 执行流程(STW): ① 选择所有 Eden Region + 所有 Survivor Region 作为收集集合(CSet) ② 从 GC Roots + RSet(跨代引用)出发,标记 CSet 中的存活对象 ③ 将存活对象复制到新的 Survivor Region(年龄 < MaxTenuringThreshold) 或晋升到 Old Region(年龄 >= MaxTenuringThreshold) ④ 清空原 Eden/Survivor Region → 变为 Free Region ⑤ 恢复用户线程 Young GC 时序图: 用户线程: ████████│ STW(通常几十ms) │████████ GC线程: │ 标记 → 复制/晋升 → 清空 → 更新RSet │

Young GC 的特点

  • 完全 STW(暂停所有用户线程)
  • 多线程并行执行(线程数 =-XX:ParallelGCThreads
  • 通常只有几十毫秒(G1 的优势)
  • G1 根据停顿时间目标(-XX:MaxGCPauseMillis)动态调整 Eden 区的大小

2.3 并发标记周期(Concurrent Marking Cycle)

触发条件:堆使用率达到 IHOP(Initial Heap Occupancy Percent,默认 45%)

这个阶段不直接回收内存,而是扫描整个堆,识别哪些老年代 Region 垃圾最多(“Garbage-First” 名字的由来),为后续 Mixed GC 提供数据。

并发标记周期各阶段: 用户线程: ████│ │████████████████████████████│ │████████████ GC线程: │1 │ 2 并发根区扫描 │ │ │ │ 3 并发标记(最长) │3 │ │ │ 4 重新标记 │ │ │ │ 5 清除(部分并发) │ │ └──┘ └──┘ STW STW 初始标记 重新标记 (与YGC搭车) (较短) 阶段1:初始标记(Initial Mark)— STW - 标记 GC Roots 直接可达的对象 - 通常"搭车"附在一次 Young GC 上(合并停顿,减少总停顿次数) - 同时设置 TAMS(Top At Mark Start),记录并发标记期间新分配对象的起始位置 阶段2:根区扫描(Root Region Scan)— 并发 - 扫描 Survivor Region,找出所有指向老年代的引用 - 必须在下次 Young GC 之前完成(否则 Young GC 需要等待) 阶段3:并发标记(Concurrent Mark)— 并发(最耗时) - 遍历整个堆,标记所有可达对象 - 与用户线程并发,使用 SATB 处理引用变化 - 计算每个 Region 的存活率(为选择 CSet 做准备) 阶段4:重新标记(Remark)— STW - 处理 SATB 缓冲区中记录的引用变化 - 完成最终的存活对象标记 阶段5:清除(Cleanup)— 部分并发 - STW:更新 Region 存活率统计,选出完全空闲的 Region 立即回收 - 并发:整理 RSet - 不移动对象,仅更新统计数据和释放完全空闲 Region

2.4 SATB:G1 的并发标记保障

G1 使用SATB(Snapshot-At-The-Beginning)方案处理并发标记期间的引用变化,与 CMS 的增量更新方案不同:

SATB 核心思想: 在并发标记开始时,对堆做一个逻辑"快照" 标记的目标是:快照时刻的存活对象集合 (无论并发期间引用如何变化,快照时刻的存活对象都不会被漏标) 实现原理(写屏障): 当用户线程执行: old_ref = obj.field; // 旧引用 obj.field = new_ref; // 修改引用 写屏障记录 old_ref(而非 new_ref): // SATB 写屏障 if (is_marking_active && old_ref != null) { satb_buffer.add(old_ref); // 记录被覆盖的旧引用 } obj.field = new_ref; 为什么记录旧引用? - 旧引用 old_ref 可能因为这次修改而断开,变成"不可达" - 但在快照时刻它是存活的,应该被当作存活处理(宁可多保留,不能漏标) - 这些被记录的旧引用在重新标记阶段重新扫描 SATB vs 增量更新(CMS): SATB:精度略低(可能多保留垃圾),但重新标记更快,适合控制停顿 增量更新:精度更高,但重新标记可能扫描更多对象

2.5 Mixed GC

触发条件:并发标记周期完成后,G1 会在后续若干次 Young GC 中穿插进行 Mixed GC

与 Young GC 的区别

  • Young GC:只回收年轻代 Region(Eden + Survivor)
  • Mixed GC:回收所有年轻代 Region +若干垃圾最多的老年代 Region
Mixed GC CSet 选择策略(Garbage-First!): 并发标记统计的老年代 Region 垃圾率: Region #12: 90% 垃圾 ★★★ Region #7: 85% 垃圾 ★★★ Region #23: 78% 垃圾 ★★★ Region #5: 65% 垃圾 ★★ Region #31: 40% 垃圾 ★ ... G1 优先选择垃圾率最高的 Region 加入 CSet: - 用最少的 GC 工作量回收最多的内存 - 这就是 "Garbage-First" 名字的真正含义! 参数控制: -XX:G1MixedGCCountTarget=8 # Mixed GC 最多执行8次(分摊老年代回收) -XX:G1HeapWastePercent=5 # 当可回收比例 < 5%,停止 Mixed GC -XX:G1OldCSetRegionThresholdPercent=10 # 每次 Mixed GC 老年代 Region 占比上限

2.6 Full GC(G1 的最后手段)

G1 的 Full GC 使用单线程的标记-整理算法(类似 Serial Old),是 G1 下最糟糕的情况:

触发条件

1. 并发标记失败(分配速度 > 回收速度,堆塞满了) → 触发 Full GC + 打印 "GC(Allocation Failure)" 或 "GC(Concurrent Mode Failure)" 2. Mixed GC 后老年代仍然不足 → 触发 Full GC 3. 元空间不足 → 触发 Full GC 4. 显式调用 System.gc()(禁用:-XX:+DisableExplicitGC)

如何避免 G1 的 Full GC(这是 G1 调优的核心目标):

# 1. 增大堆,给并发标记足够的时间-Xmx8g# 根据实际需要调整# 2. 降低 IHOP,更早触发并发标记-XX:InitiatingHeapOccupancyPercent=35# 35% 时就开始并发标记(默认45%)# 3. 增大 G1 新生代比例上限,避免过于激进的晋升-XX:G1NewSizePercent=5# 年轻代最小占比(默认5%)-XX:G1MaxNewSizePercent=40# 年轻代最大占比(默认60%,可适当调小)

三、RSet(Remembered Set):跨 Region 引用追踪

3.1 为什么需要 RSet

G1 每次 GC 只处理部分 Region(CSet),但 CSet 外的 Region 可能持有对 CSet 内对象的引用。如果不追踪这些跨 Region 引用,就无法正确判断 CSet 中对象是否存活。

没有 RSet 的问题: CSet:{Region 1} Region 2(非CSet) └──→ 对象 X(在 Region 1 中) 如果不知道 Region 2 引用了 X,X 会被误当成垃圾回收!

3.2 RSet 的工作原理

每个 Region 维护一个Remembered Set(RSet),记录"哪些其他 Region 中的对象引用了我这个 Region 中的对象":

RSet 结构(以 Region 1 的 RSet 为例): Region 1 的 RSet: {Region 5: 卡 #3, #7} ← Region 5 的第3、7张卡中有指向 Region 1 的引用 {Region 12: 卡 #1} ← Region 12 的第1张卡中有指向 Region 1 的引用 GC 时利用 RSet: 扫描 Region 1 的 RSet 列出的所有卡 → 找出跨 Region 引用 → 加入 GC Roots 这样就不用扫描整个堆了! RSet 维护: 通过写屏障(Post-Write Barrier)在引用写入时更新 RSet: obj.field = ref; // 如果 obj 和 ref 在不同 Region,更新 ref 所在 Region 的 RSet

3.3 RSet 的内存开销

RSet 不是免费的,它会占用一定的堆内存(通常 5%~20%):

# 监控 RSet 相关统计-XX:+G1SummarizeRSetStats# 打印 RSet 统计信息-XX:G1SummarizeRSetStatsPeriod=1# 每1次 GC 打印一次# RSet 精细度(PRT = Per-Region Table)# G1 会根据引用数量自动选择:# Fine(精细):< 3 个引用# Coarse(粗糙):引用过多时退化为位图,精度降低

四、G1 的停顿时间预测模型

4.1 如何"预测"停顿时间

G1 通过历史统计数据预测每个 Region 的 GC 耗时,从而在满足停顿时间目标(-XX:MaxGCPauseMillis)的前提下,尽量多回收 Region:

G1 停顿时间预测(衰减平均值模型): 对每个 Region,G1 统计: - 扫描 RSet 的时间 - 复制存活对象的时间(基于对象大小、存活率) 每次 GC 前,G1 预测: 预计停顿时间 = 固定开销 + Σ(各 Region 的预计耗时) G1 贪心地选择 Region 加入 CSet,直到: 预计停顿时间 ≈ MaxGCPauseMillis(停顿时间目标) 如果选完所有年轻代 Region 后预计停顿已超过目标: → G1 可能动态缩小年轻代(减少 Eden Region 数量)

4.2 停顿时间目标的设定建议

# 停顿时间目标(默认200ms,对大多数应用合适)-XX:MaxGCPauseMillis=200# 实践建议:# 在线服务(用户交互):50~200ms# 批处理/后台服务:可以设置 500~1000ms(允许更大停顿,换取更少 GC 次数)# 延迟敏感(实时系统):建议使用 ZGC/Shenandoah# ⚠️ 注意:这是"软目标",G1 会尽力保证,但不是严格保证# 如果必须触发 Full GC,停顿时间可能远超此值

五、G1 完整调优参数

5.1 核心参数

# ===== 启用 G1 =====-XX:+UseG1GC# JDK 9+ 默认,JDK 8 需显式指定# ===== 停顿时间目标(最重要!)=====-XX:MaxGCPauseMillis=200# 目标最大停顿时间 200ms(默认200ms)# ===== 堆大小 =====-Xms4g# 初始堆大小-Xmx4g# 最大堆大小(建议与Xms相同)# ===== Region 大小 =====-XX:G1HeapRegionSize=4m# Region 大小(默认自动计算,通常不需要设置)# ===== 并发标记触发时机 =====-XX:InitiatingHeapOccupancyPercent=45# 堆使用率达到45%触发并发标记(默认45%)# 如果 Full GC 频繁,可降低到 35%# ===== 年轻代大小控制 =====-XX:G1NewSizePercent=5# 年轻代最小占比(默认5%)-XX:G1MaxNewSizePercent=60# 年轻代最大占比(默认60%)# ===== 老年代晋升 =====-XX:MaxTenuringThreshold=15# 对象晋升老年代年龄阈值-XX:G1ReservePercent=10# 老年代预留空间百分比(防止晋升失败)

5.2 Mixed GC 控制参数

# Mixed GC 控制-XX:G1HeapWastePercent=5# 可回收垃圾 < 5% 时停止 Mixed GC(默认5%)-XX:G1MixedGCCountTarget=8# Mixed GC 最多连续触发8次(默认8次)-XX:G1MixedGCLiveThresholdPercent=85# 老年代 Region 存活率 > 85% 不纳入 CSet(默认85%)-XX:G1OldCSetRegionThresholdPercent=10# 每次 Mixed GC 老年代 Region 数量上限(占总 Region 10%)

5.3 GC 线程数

-XX:ParallelGCThreads=8# STW 阶段并行 GC 线程数(默认 = CPU核数,最多8)-XX:ConcGCThreads=4# 并发标记线程数(默认 = ParallelGCThreads/4)

5.4 日志与诊断

# JDK 9+ 统一日志格式-Xlog:gc*:file=/var/log/jvm/gc.log:time,level,tags:filecount=5,filesize=20m# JDK 8 格式-XX:+PrintGCDetails-XX:+PrintGCDateStamps-Xloggc:/var/log/jvm/gc.log# G1 专项诊断-XX:+G1PrintRegionLivenessInfo# 打印每个 Region 的存活信息-XX:+G1SummarizeRSetStats# 打印 RSet 统计

六、G1 生产环境调优案例

6.1 典型配置:JDK 17 在线服务(8核16GB)

# 适合:Spring Boot 微服务,要求 P99 响应 < 500msjava-XX:+UseG1GC\-Xms8g\-Xmx8g\-XX:MaxGCPauseMillis=200\-XX:InitiatingHeapOccupancyPercent=35\-XX:G1HeapRegionSize=4m\-XX:G1ReservePercent=15\-XX:ParallelGCThreads=8\-XX:ConcGCThreads=4\-XX:+HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath=/var/log/jvm/\-Xlog:gc*:file=/var/log/jvm/gc.log:time,level,tags:filecount=5,filesize=20m\-jarapp.jar

6.2 Full GC 排查思路

G1 出现 Full GC 时的排查步骤: 1. 查看 GC 日志,确认 Full GC 原因: "GC(Allocation Failure)" → 分配速度过快,并发标记来不及 "GC(Metadata GC Threshold)" → 元空间触发 "GC(Ergonomics)" → G1 自适应触发 2. 根据原因对症下药: 分配速度过快 → 降低 IHOP / 增大堆 / 排查内存泄漏 元空间触发 → 增大 -XX:MaxMetaspaceSize / 排查 ClassLoader 泄漏 3. 常用监控命令: jstat -gcutil <pid> 1000 # 每1秒打印 GC 统计 jmap -heap <pid> # 堆内存使用情况

七、G1 vs CMS:为什么 G1 赢了

对比维度CMSG1
内存碎片有(标记-清除)无(复制/整理)
停顿时间可预测性不可预测可设定目标
Full GC 风险Concurrent Mode Failure 可能发生较少,但可能更长
大堆支持堆越大 STW 越长Region 化,大堆表现更好
内存开销较低较高(RSet + SATB缓冲区)
适用堆大小< 4GB 效果好>= 4GB 有明显优势
JDK 状态JDK 9 废弃,JDK 14 移除JDK 9+ 默认

结论:对于新项目,G1 应该是 JDK 8u40+ 及以上版本的首选 GC(大堆、在线服务场景)。JDK 8 配合 G1 + 细心调优,完全可以替代 CMS + ParNew 组合。


八、总结

G1 是传统分代 GC 向现代 Region 化 GC 的里程碑式转变:

  • Region 分区:打破物理连续分代,每个 Region 角色动态分配,支持大堆
  • Humongous 对象:≥ Region 50% 的对象直接进老年代专用 Region,避免频繁复制
  • 四种 GC 类型:Young GC(纯年轻代)→ 并发标记周期(识别垃圾 Region)→ Mixed GC(年轻代+精选老年代)→ Full GC(最后手段)
  • SATB 写屏障:记录并发标记期间被修改的旧引用,保证不漏标存活对象
  • RSet:每个 Region 维护的跨 Region 引用追踪表,GC 时避免扫描全堆
  • 停顿时间目标:基于历史统计的预测模型,让 GC 停顿可控、可预期

下一篇预告:如果 G1 的 200ms 停顿时间目标仍然无法满足你的业务需求,那就需要认识 ZGC——一个将 GC 停顿压缩到 10ms 以内的革命性收集器。ZGC 用染色指针和读屏障取代了传统的写屏障模式,实现了真正的"并发整理"。第07篇将带你深入 ZGC 的内部世界。


系列导航

  • 上一篇:【JVM深度解析】第05篇:传统垃圾收集器总览
  • 下一篇:【JVM深度解析】第07篇:ZGC垃圾收集器深度解析
  • 系列目录:JVM深度解析系列全集

参考资料

  1. 《深入理解Java虚拟机(第3版)》第3章 — 周志明著
  2. G1 GC Tuning Guide — Oracle
  3. JEP 248: Make G1 the Default Garbage Collector
  4. Getting Started with the G1 Garbage Collector — Oracle
  5. Monica Beckwith: G1 GC Best Practices
  6. Aleksey Shipilёv: Garbage-First Garbage Collector Notes

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 0:06:53

大模型微调技术精要:小白程序员必备,助你秋招收藏必备!

本文从大模型微调技术概要出发&#xff0c;结合秋招面试高频问题&#xff0c;详细解析了LoRA微调原理、Prompt微调技术以及参数高效微调PEFT和指令微调SFT等核心知识点。文章还涵盖了微调技术选型、Loss计算、数据集收集、避免灾难性遗忘等实用技巧&#xff0c;并介绍了fp16和b…

作者头像 李华
网站建设 2026/4/17 0:06:03

java.lang.UnsupportedClassVersionError

引言:一个看似简单却暗藏玄机的错误 在 Java 开发与运维的日常中,开发者们常常会遇到形形色色的异常。有些异常如 NullPointerException 航行于代码逻辑的浅滩,容易定位;而另一些则如 java.lang.UnsupportedClassVersionError,它潜伏在编译与运行环境的交界处,像一道无形…

作者头像 李华
网站建设 2026/4/17 0:05:26

如何统一SQL视图报错信息_使用异常处理机制包装视图

SQL Server视图不支持TRY…CATCH&#xff0c;需用存储过程包装并加异常处理&#xff1b;PostgreSQL可用函数EXCEPTION块实现&#xff0c;返回TABLE或JSONB&#xff1b;应用层仍需统一捕获和分类错误。SQL Server 视图里没法直接写 TRY…CATCH&#xff1f;对&#xff0c;TRY...C…

作者头像 李华
网站建设 2026/4/17 0:03:22

云边端一体化优势:低延迟、高可靠、省带宽的核心逻辑

云边端一体化优势&#xff1a;低延迟、高可靠、省带宽的核心逻辑&#x1f4da; 本章学习目标&#xff1a;深入理解低延迟、高可靠、省带宽的核心逻辑的核心概念与实践方法&#xff0c;掌握关键技术要点&#xff0c;了解实际应用场景与最佳实践。本文属于《云原生、云边端一体化…

作者头像 李华