【JVM深度解析】第06篇:G1垃圾收集器深度解析
摘要G1Garbage-First垃圾收集器是 JDK 9 的默认 GC通过将堆划分为等大小的 Region 区域彻底摒弃了传统的物理连续分代结构实现了可预测的停顿时间模型。本文深入解析 G1 的核心设计Region 分区机制与 Humongous 对象处理、四种 GC 类型Young GC/Mixed GC/Concurrent Cycle/Full GC的触发条件与执行流程、SATBSnapshot-At-The-Beginning写屏障的并发标记方案、RSetRemembered Set的跨 Region 引用追踪以及停顿时间预测模型的实现原理。附完整的 G1 调优参数详解与生产环境最佳实践帮助开发者充分发挥 G1 的能力。引言当你的服务堆内存超过 4GBFull 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/20482M 每 Region-Xmx8g→ 8192M/20484M 每 Region-Xmx16g→ 16384M/20488M 每 Region 手动指定一般不需要-XX:G1HeapRegionSize4m# 指定 Region 大小1.3 Humongous 对象大对象处理当对象大小≥ Region 大小的 50%时被视为 Humongous 对象直接分配到老年代中的专用 Humongous RegionRegion 大小 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触发条件堆使用率达到 IHOPInitial 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 上合并停顿减少总停顿次数 - 同时设置 TAMSTop 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 - 不移动对象仅更新统计数据和释放完全空闲 Region2.4 SATBG1 的并发标记保障G1 使用SATBSnapshot-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只回收年轻代 RegionEden SurvivorMixed GC回收所有年轻代 Region 若干垃圾最多的老年代 RegionMixed 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:G1MixedGCCountTarget8 # Mixed GC 最多执行8次分摊老年代回收 -XX:G1HeapWastePercent5 # 当可回收比例 5%停止 Mixed GC -XX:G1OldCSetRegionThresholdPercent10 # 每次 Mixed GC 老年代 Region 占比上限2.6 Full GCG1 的最后手段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:InitiatingHeapOccupancyPercent35# 35% 时就开始并发标记默认45%# 3. 增大 G1 新生代比例上限避免过于激进的晋升-XX:G1NewSizePercent5# 年轻代最小占比默认5%-XX:G1MaxNewSizePercent40# 年轻代最大占比默认60%可适当调小三、RSetRemembered Set跨 Region 引用追踪3.1 为什么需要 RSetG1 每次 GC 只处理部分 RegionCSet但 CSet 外的 Region 可能持有对 CSet 内对象的引用。如果不追踪这些跨 Region 引用就无法正确判断 CSet 中对象是否存活。没有 RSet 的问题 CSet{Region 1} Region 2非CSet └──→ 对象 X在 Region 1 中 如果不知道 Region 2 引用了 XX 会被误当成垃圾回收3.2 RSet 的工作原理每个 Region 维护一个Remembered SetRSet记录哪些其他 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 的 RSet3.3 RSet 的内存开销RSet 不是免费的它会占用一定的堆内存通常 5%~20%# 监控 RSet 相关统计-XX:G1SummarizeRSetStats# 打印 RSet 统计信息-XX:G1SummarizeRSetStatsPeriod1# 每1次 GC 打印一次# RSet 精细度PRT Per-Region Table# G1 会根据引用数量自动选择# Fine精细 3 个引用# Coarse粗糙引用过多时退化为位图精度降低四、G1 的停顿时间预测模型4.1 如何预测停顿时间G1 通过历史统计数据预测每个 Region 的 GC 耗时从而在满足停顿时间目标-XX:MaxGCPauseMillis的前提下尽量多回收 RegionG1 停顿时间预测衰减平均值模型 对每个 RegionG1 统计 - 扫描 RSet 的时间 - 复制存活对象的时间基于对象大小、存活率 每次 GC 前G1 预测 预计停顿时间 固定开销 Σ(各 Region 的预计耗时) G1 贪心地选择 Region 加入 CSet直到 预计停顿时间 ≈ MaxGCPauseMillis停顿时间目标 如果选完所有年轻代 Region 后预计停顿已超过目标 → G1 可能动态缩小年轻代减少 Eden Region 数量4.2 停顿时间目标的设定建议# 停顿时间目标默认200ms对大多数应用合适-XX:MaxGCPauseMillis200# 实践建议# 在线服务用户交互50~200ms# 批处理/后台服务可以设置 500~1000ms允许更大停顿换取更少 GC 次数# 延迟敏感实时系统建议使用 ZGC/Shenandoah# ⚠️ 注意这是软目标G1 会尽力保证但不是严格保证# 如果必须触发 Full GC停顿时间可能远超此值五、G1 完整调优参数5.1 核心参数# 启用 G1 -XX:UseG1GC# JDK 9 默认JDK 8 需显式指定# 停顿时间目标最重要-XX:MaxGCPauseMillis200# 目标最大停顿时间 200ms默认200ms# 堆大小 -Xms4g# 初始堆大小-Xmx4g# 最大堆大小建议与Xms相同# Region 大小 -XX:G1HeapRegionSize4m# Region 大小默认自动计算通常不需要设置# 并发标记触发时机 -XX:InitiatingHeapOccupancyPercent45# 堆使用率达到45%触发并发标记默认45%# 如果 Full GC 频繁可降低到 35%# 年轻代大小控制 -XX:G1NewSizePercent5# 年轻代最小占比默认5%-XX:G1MaxNewSizePercent60# 年轻代最大占比默认60%# 老年代晋升 -XX:MaxTenuringThreshold15# 对象晋升老年代年龄阈值-XX:G1ReservePercent10# 老年代预留空间百分比防止晋升失败5.2 Mixed GC 控制参数# Mixed GC 控制-XX:G1HeapWastePercent5# 可回收垃圾 5% 时停止 Mixed GC默认5%-XX:G1MixedGCCountTarget8# Mixed GC 最多连续触发8次默认8次-XX:G1MixedGCLiveThresholdPercent85# 老年代 Region 存活率 85% 不纳入 CSet默认85%-XX:G1OldCSetRegionThresholdPercent10# 每次 Mixed GC 老年代 Region 数量上限占总 Region 10%5.3 GC 线程数-XX:ParallelGCThreads8# STW 阶段并行 GC 线程数默认 CPU核数最多8-XX:ConcGCThreads4# 并发标记线程数默认 ParallelGCThreads/45.4 日志与诊断# JDK 9 统一日志格式-Xlog:gc*:file/var/log/jvm/gc.log:time,level,tags:filecount5,filesize20m# 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:MaxGCPauseMillis200\-XX:InitiatingHeapOccupancyPercent35\-XX:G1HeapRegionSize4m\-XX:G1ReservePercent15\-XX:ParallelGCThreads8\-XX:ConcGCThreads4\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/var/log/jvm/\-Xlog:gc*:file/var/log/jvm/gc.log:time,level,tags:filecount5,filesize20m\-jarapp.jar6.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深度解析系列全集参考资料《深入理解Java虚拟机第3版》第3章 — 周志明著G1 GC Tuning Guide — OracleJEP 248: Make G1 the Default Garbage CollectorGetting Started with the G1 Garbage Collector — OracleMonica Beckwith: G1 GC Best PracticesAleksey Shipilёv: Garbage-First Garbage Collector Notes