1. 从一次线上告警说起为什么我们需要深入了解G1 GC那天凌晨手机突然响起刺耳的告警声。线上一个核心服务的响应时间曲线像坐了火箭一样直线飙升从平时的50毫秒瞬间突破了5秒。登录服务器一看CPU使用率并不高但内存使用率却居高不下Full GC的日志像瀑布一样刷屏每次停顿都接近10秒。业务几乎陷入停滞。经过一番紧急排查根源指向了JVM垃圾回收器——我们当时使用的是CMS但在堆内存达到32GB、且对象分配速率极高的场景下它已经力不从心出现了“并发模式失败”最终导致了那次严重的服务雪崩。这次事故让我下定决心必须彻底搞懂一个更适应现代大内存、多核处理器应用的垃圾回收器G1Garbage-First。它从JDK 9开始成为默认的垃圾回收器取代了Parallel Scavenge和CMS。但很多开发者对它的认知可能还停留在“默认的、更先进的回收器”这个层面对于其内部原理、关键参数如何调优依然雾里看花。结果就是要么沿用默认参数“躺平”遇到性能问题束手无策要么胡乱调整几个-XX参数效果可能适得其反。这篇文章我想从一个一线开发/运维的视角拆解G1 GC的核心工作机制。我不会只讲教科书上的概念而是结合我多次在真实生产环境中调试、踩坑、最终稳定服务的经验告诉你G1到底是怎么工作的它的那些关键参数比如-XX:MaxGCPauseMillis、-XX:G1HeapRegionSize背后真正的含义是什么以及在不同场景高吞吐批处理、低延迟在线服务、大内存应用下我们应该如何有的放矢地进行设置和调优。目标很明确让你不仅能看懂G1的日志更能真正驾驭它让你负责的服务跑得更稳、更快。2. G1 GC的核心设计思想化整为零与可预测的停顿在G1出现之前主要的垃圾回收器如Serial Parallel CMS都采用分代收集并且堆内存的布局是连续的分为年轻代Young Generation和老年代Old Generation。这种布局在堆很大时比如几十GB进行一次全局的垃圾回收尤其是Full GC停顿时间会非常长且不可预测。G1采用了一种革命性的设计思路将连续的Java堆划分为多个大小相等、物理上不连续的独立区域Region。每个Region可以是Eden区、Survivor区或Old区但它们的角色在运行期可以动态变化。这种设计的精妙之处在于G1不再坚持一次收集整个年轻代或整个老年代而是优先去回收那些垃圾最多Garbage-First名字的由来、回收收益最高的Region集合。通过每次只处理一部分Region将原本一次漫长的GC停顿分散到多次短暂的、可控的停顿中从而实现了可预测的停顿时间模型。2.1 Region堆内存管理的基本单元Region的大小可以通过-XX:G1HeapRegionSize指定范围从1M到32M且必须是2的幂。JVM会基于堆的初始大小和最大大小自动计算一个合理的值通常是堆大小的1/2000左右。例如你设置了-Xmx16gJVM可能会选择Region大小为8MB16GB / 2000 ≈ 8MB。注意Region大小设置需要权衡。Region太小会导致Region数量过多增加管理开销Region太大可能导致大对象分配问题后面会讲。通常不建议手动设置除非有明确需求比如为了适配特定的大对象。每个Region都通过一个Remembered SetRSet来记录来自其他Region的引用。这是G1实现并发的关键。因为G1在做部分Region收集即Mixed GC时需要知道哪些外部对象引用了当前Region内的存活对象以避免扫描整个堆。RSet就像一个“外来人口登记簿”极大地缩小了扫描范围。2.2 G1的运作阶段不是简单的Young/Old GCG1的收集活动不像CMS那样清晰地分为Young GC和Old GC。它主要有三种收集类型Young GC当Eden区被填满时触发。这时G1会暂停所有应用线程Stop-The-World将Eden区和Survivor区From的存活对象拷贝到新的Survivor区To或晋升到Old区的Region中。这个过程是并行多线程的旨在高效利用多核CPU。Mixed GC这是G1实现“可预测停顿”的核心。当堆内存使用率达到一定阈值默认45%由-XX:InitiatingHeapOccupancyPercent控制时G1会启动一个并发标记周期。这个周期结束后G1就知道了哪些Region的垃圾比例高。随后的Mixed GC就会在回收所有年轻代RegionEdenSurvivor的同时选择性地回收一部分被标记为“高垃圾比例”的老年代Region。Mixed GC也是STW的但通过控制回收的Region数量可以努力达到我们设定的停顿时间目标。Full GC这是我们要极力避免的。当G1在进行并发标记时如果对象分配速度太快导致在Mixed GC还没来得及回收足够空间时堆就被填满了这时就会触发一次单线程的、STW的Full GC性能灾难就此发生。调优的核心目标之一就是避免Full GC。3. 关键参数详解与调优实战理解了核心思想我们来看那些让人眼花缭乱的JVM参数。我不打算罗列所有参数只聚焦那些对性能有决定性影响的关键开关。3.1 停顿时间目标-XX:MaxGCPauseMillis这是G1调优的“指挥棒”默认值是200毫秒。它告诉G1“我希望每次GC停顿的时间尽量不超过这个值。”重要理解这个参数是一个目标而非硬性保证。G1会努力通过调整每次回收的Region数量主要是年轻代的大小来逼近这个目标。如果你设了一个不切实际的目标比如20msG1可能会将年轻代设置得非常小导致GC频率急剧升高虽然每次停顿短了但总体吞吐量会严重下降。调优建议不要盲目设小对于大多数延迟不敏感的后台服务或批处理任务200ms或甚至300ms都是可以接受的这样可以换取更高的吞吐量。如何确定合理值监控系统现有的GC日志观察“正常”情况下Young GC和Mixed GC的实际停顿时间[Eden: ...-... Survivors: ...-... Heap: ...]后面的时间。以此为基准设置一个略高于平均值的MaxGCPauseMillis给G1一些缓冲空间。例如观测到Young GC平均停顿80ms可以设置为100-150ms。配合监控设置后必须持续监控G1 Young Generation的G1 Eden Space的容量变化以及GC频率。如果发现Eden区被压得非常小GC极其频繁说明目标可能过于激进。3.2 并发标记触发阈值-XX:InitiatingHeapOccupancyPercent这个参数简称IHOP控制何时启动并发标记周期默认是45%。意思是当整个堆的使用率达到45%时G1会开始后台的并发标记为后续的Mixed GC做准备。为什么这个参数至关重要并发标记需要时间通常不短。如果触发得太晚比如堆都用到80%了才开始标记可能在标记完成前应用就已经把堆分配满了从而直接触发Full GC。如果触发得太早又会不必要地占用CPU资源进行标记影响应用吞吐。调优建议对于对象分配速率高或堆内存很大比如超过32G的应用建议适当降低IHOP。例如设置为35%或40%给并发标记留出更充裕的时间窗口。在JDK 9u40之后G1引入了自适应IHOP通过-XX:G1UseAdaptiveIHOP开启默认开启。G1会学习应用的行为自动调整IHOP。在应用启动稳定后这个功能通常效果很好。但在应用启动阶段或负载发生剧烈变化时手动设置一个保守值作为起点更安全。3.3 并行线程数-XX:ParallelGCThreads与-XX:ConcGCThreadsParallelGCThreads控制STW阶段Young GC、Mixed GC的转移阶段进行并行垃圾回收的线程数。默认值基于CPU核心数计算ParallelGCThreads (ncpus 8) ? ncpus : (8 ((ncpus - 8) * 5 / 8))。对于多核机器通常无需调整。ConcGCThreads控制并发阶段并发标记、并发清理使用的线程数。默认值是ParallelGCThreads / 4。调优建议除非你非常清楚自己在做什么否则不要轻易改动。增加线程数会加快GC本身但也会争夺应用线程的CPU资源。一种典型的调优场景是在CPU资源非常充裕比如容器分配了8核但应用平时只用4核且GC停顿是主要瓶颈时可以尝试略微增加ConcGCThreads例如设为ParallelGCThreads / 2以加快并发标记速度降低因标记跟不上分配而引发Full GC的风险。3.4 大对象处理-XX:G1HeapRegionSize与 Humongous Region如果一个对象的大小超过了一个Region大小的50%它就会被认为是一个“巨型对象”Humongous Object。G1会为它分配连续的多个Region这些Region被标记为Humongous Region且在回收时只有在Full GC时才会被处理。这意味着什么如果你的应用有大量短命的大对象比如某个接口频繁分配几百KB的临时数组它们会迅速占满Humongous Region但又不会在常规的Young/Mixed GC中被回收从而可能提前触发并发标记甚至Full GC。调优实战监控查看GC日志关注Humongous allocations和Humongous regions相关的信息。分析如果发现Humongous分配频繁且对象生命周期很短。对策优化代码这是根本。检查是否可以通过对象复用、调整数据结构来避免分配如此大的对象。调整Region大小如果大对象的大小刚好略高于RegionSize的50%可以考虑适当增大G1HeapRegionSize使得这些对象不再被判定为Humongous从而能被正常的GC回收。例如默认RegionSize是8M一个4.1M的对象就是Humongous如果将RegionSize设为16M它就不是了。权衡增大RegionSize会减少Region总数但可能影响内存的灵活分配。需要经过测试。4. 一份生产级G1配置模板与参数解析说了这么多理论来看一个我用于某个中等延迟要求的微服务堆内存16G的配置示例。这不是银弹但可以作为一个理解和调整的起点。# 堆内存设置 -Xms8g -Xmx8g # 生产环境建议初始堆和最大堆设置相同避免运行时扩容带来的性能波动。 # 使用G1垃圾回收器 -XX:UseG1GC # 核心停顿时间目标根据监控调整 -XX:MaxGCPauseMillis150 # 并发标记触发阈值为标记留出足够时间 -XX:InitiatingHeapOccupancyPercent35 # 启用并行GC线程数自动调整一般保持默认 # -XX:ParallelGCThreads... # 稍微增加并发标记线程加快标记速度在16核机器上 -XX:ConcGCThreads4 # 启用GC日志这是调优的“眼睛”必须开启 -XX:PrintGCDetails -XX:PrintGCDateStamps -XX:PrintGCTimeStamps -XX:PrintAdaptiveSizePolicy # 打印G1自适应调整的决策对理解行为很有帮助 -Xloggc:/path/to/your/service_gc.log # 指定GC日志路径 # 当日志文件达到一定大小后滚动避免撑爆磁盘 -XX:UseGCLogFileRotation -XX:NumberOfGCLogFiles10 -XX:GCLogFileSize100M # 在发生OOM时生成堆转储便于事后分析 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/your/heap_dump.hprof # 禁用偏向锁在并发高的微服务中通常能提升性能 -XX:-UseBiasedLocking # 启用字符串去重JDK 8u20节省内存但会消耗少量CPU -XX:UseStringDeduplication逐项解析与心得-Xms和-Xmx相等这可能是最重要的一个最佳实践。避免堆在运行时自动伸缩这会导致不必要的GC和性能不稳定。PrintAdaptiveSizePolicy这个日志开关非常有用。它会打印G1为什么调整年轻代大小例如“[G1Ergonomics (Heap Sizing) attempt heap expansion...”让你看清G1是如何努力达到停顿时间目标的。UseStringDeduplication对于处理大量文本如JSON/XML解析的服务这个功能可以节省可观的内存。它通过额外的并发线程来查找并合并重复的字符串。如果你的应用CPU非常紧张可以关闭它。5. 从GC日志诊断常见问题与排查技巧光有配置不够必须学会看GC日志。下面是一段典型的Mixed GC日志我们一起来解读2024-05-27T10:23:15.1230800: 1023.456: [GC pause (G1 Evacuation Pause) (mixed) [Eden: 4096.0M(4096.0M)-0.0B(4096.0M) Survivors: 1024.0M-1024.0M Heap: 12.5G(16.0G)-10.1G(16.0G)] [Times: user1.23 sys0.12, real0.25 secs]1023.456从JVM启动到现在的秒数。G1 Evacuation Pause (mixed)这是一次Mixed GC。Eden: 4096M-0B回收前Eden区有4G回收后为0。Survivors: 1024M-1024MSurvivor区大小不变但内部对象已经过拷贝和晋升。Heap: 12.5G-10.1G整个堆回收前用了12.5G回收后用了10.1G释放了2.4G空间。[Times: user1.23, real0.25]这是关键user时间是所有GC线程消耗的CPU时间总和1.23秒real是实际的STW停顿时间0.25秒。因为GC是多线程并行工作所以real时间远小于user时间。如果real时间持续超过MaxGCPauseMillis就需要关注了。常见问题排查清单问题现象可能原因排查方向与调优建议Young GC停顿时间过长1. 每次回收的存活对象太多拷贝开销大。2.-XX:MaxGCPauseMillis设置太小导致年轻代过大单次回收区域大。1. 检查Survivor区晋升到Old区的对象是否过多、过早。可尝试调大Survivor区-XX:SurvivorRatio默认8可调小如6或提高晋升阈值-XX:MaxTenuringThreshold默认15。2. 适当放宽MaxGCPauseMillis目标。Mixed GC频率低但每次回收不多并发标记效率低或者IHOP设置不合理导致老年代垃圾堆积。1. 检查GC日志中并发标记阶段的耗时。2.降低-XX:InitiatingHeapOccupancyPercent让标记更早开始。3.增加-XX:ConcGCThreads加速并发标记。频繁发生Full GC1. 对象分配速率远超回收速率“并发模式失败”。2. Humongous对象堆积。3. 内存碎片化严重虽然G1有整理但极端情况仍可能发生。1.根本解决优化代码降低内存分配速率。2.应急增加堆内存-Xmx。3. 检查Humongous分配考虑调整G1HeapRegionSize或优化大对象使用。4. 确保-Xms和-Xmx相等。应用吞吐量显著下降GC线程占用了过多CPU资源。1. 检查user时间是否过高。如果real停顿不长但user很高说明GC线程抢占了太多CPU。2. 考虑减少-XX:ParallelGCThreads谨慎操作。3. 关闭-XX:UseStringDeduplication等消耗CPU的优化特性。GC日志中出现“to-space exhausted”Survivor区或老年代没有足够空间容纳晋升的对象。这是Full GC的前兆。立即检查内存使用和对象分配模式。可能需要增加堆大小或优化对象结构减少晋升。一个关键的实操心得调优是一个“观察-假设-调整-验证”的循环。永远不要一次性修改多个参数。每次只调整一个你认为最可能解决问题的参数然后收集足够长时间的监控数据至少覆盖一个完整的业务高峰周期来观察效果。使用APM工具如Pinpoint, SkyWalking的JVM监控面板结合GC日志分析工具如GCeasy, G1GCViewer可以让你事半功倍。最后我想强调的是G1是一个高度自动化的、追求平衡的垃圾回收器。很多时候“少即是多”。在充分理解其原理和监控数据之前使用JVM默认的参数或仅做微调往往比盲目套用网上搜来的“优化参数”要好得多。我的经验是先给足内存-Xmx设置一个合理的停顿目标MaxGCPauseMillis并提前触发标记降低IHOP这三点做好大部分应用的GC表现就已经在及格线以上了。剩下的精细调优需要结合你独一无二的应用特性和负载模式来进行。