尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

G1垃圾回收器:原理、调优与低延迟实践指南

G1垃圾回收器:原理、调优与低延迟实践指南 1. 从CMS到G1为什么我们需要新一代的垃圾回收器如果你在Java世界里摸爬滚打超过五年大概率经历过从Parallel Scavenge/Parallel OldPS/PO到CMS再到G1的变迁。早些年堆内存还没那么大业务对停顿时间STW也没那么敏感PS/PO这种追求吞吐量的“大力出奇迹”型回收器是主流。后来随着互联网应用爆发动辄几个G甚至几十个G的堆内存成为常态一次Full GC停顿几十秒甚至几分钟用户体验直接崩盘。于是以“低延迟”为目标的CMSConcurrent Mark-Sweep回收器登上了历史舞台。CMS确实在很长一段时间里是解决延迟问题的首选。它的核心思想是把最耗时的标记Mark和清除Sweep动作尽可能与用户线程并发执行只在初始标记和重新标记阶段有短暂的STW。但用久了你就会发现它的“阿喀琉斯之踵”内存碎片和不可预测的并发模式失败Concurrent Mode Failure。内存碎片会导致明明堆里还有不少空闲内存却因为找不到一块连续的空间来分配大对象而不得不触发一次“兜底”的Full GC通常是Serial Old那停顿时间瞬间回到解放前。而并发模式失败则是在并发回收过程中用户线程产生的“浮动垃圾”速度超过了预期导致回收速度赶不上分配速度同样会触发Full GC。所以当堆内存越来越大业务对延迟的要求越来越苛刻比如要求99.9%的请求停顿时间在100毫秒以内时CMS就显得力不从心了。我们需要一个能同时兼顾吞吐量和延迟并且能有效管理大内存、避免碎片化的新方案。这就是G1Garbage-First垃圾回收器诞生的背景。它不是对CMS的小修小补而是一种设计理念上的革新将堆内存划分为多个大小相等的区域Region并引入可预测的停顿时间模型目标是在有限的停顿时间内回收尽可能多的垃圾。简单说G1试图回答一个问题给定一个停顿时间目标比如200ms我如何最高效地清理垃圾2. G1的核心设计思想化整为零与价值优先G1的设计非常精妙理解它的两个核心思想是掌握其工作原理的关键。2.1 区域化内存布局Region这是G1与之前所有回收器最根本的不同。它不再坚持传统的连续新生代、老年代物理划分。而是把整个Java堆除了巨型对象专用的Humongous Region划分成多个大小固定默认约1MB~32MB可调的Region。每个Region在逻辑上被标记为Eden、Survivor或Old但这个身份不是固定的在一次回收后一个Region完全可能从Eden变成Survivor或者从Survivor变成Old。注意Region的大小通过-XX:G1HeapRegionSize指定必须是2的幂次方范围在1MB到32MB之间。JVM会基于初始堆大小自动计算一个合理的值。这个值会影响巨型对象的判定对象大小超过Region大小的50%即被认定为巨型对象存入连续的Humongous Region设置不当可能会影响大对象分配效率。这种设计的优势显而易见避免了内存碎片因为回收是以Region为最小单位进行的回收后的Region是完整的、连续的空间可以直接用于分配新对象从根本上解决了CMS的碎片化问题。实现了更精细的内存管理回收器可以不用一次处理整个新生代或老年代而是每次只选择一部分Region进行回收这为实现可预测的停顿时间打下了基础。简化了收集过程无论是Young GC还是Mixed GC其本质都是对选定Region集合的回收逻辑上得到了统一。2.2 可预测的停顿时间模型与回收价值这是G1名字中“Garbage-First”的由来也是其灵魂。G1会持续跟踪每个Region中垃圾的“含量”即存活对象所占的比例并维护一个“价值”列表。这个价值可以简单理解为回收这个Region所能获得的空间大小与回收它所需要的时间的比值。在每次需要触发垃圾回收时无论是Young GC还是Mixed GCG1的回收目标不再是“回收所有垃圾”而是“在用户设定的最大停顿时间-XX:MaxGCPauseMillis默认200ms内回收掉价值最高的那些Region”。它像一个精明的项目经理在有限的时间预算内优先处理那些“性价比”最高垃圾最多、回收最快的任务。这个模型带来了革命性的变化应用程序可以主动设定一个停顿时间目标G1会尽力去达成。虽然它不能保证每次停顿都绝对小于这个值因为总有不可预测的因素但长期来看停顿时间的分布会趋于稳定和可控。这对于需要稳定服务响应的在线应用来说价值巨大。3. G1的工作流程详解不止是Young GC和Mixed GC很多人把G1的工作简单理解为“Young GC Mixed GC”这其实忽略了它背后复杂的并发标记周期。G1的完整工作周期是Young GC与并发标记周期交替进行。3.1 年轻代回收Young GC当Eden区的Region被占满时就会触发一次Young GC。这个过程是完全STW的。根扫描暂停所有用户线程从GC Roots栈、寄存器、全局变量等开始扫描出初始的存活对象。更新记忆集RSet和处理根处理Dirty Card Queue更新相关Region的RSet。RSet是G1的另一个核心数据结构它像一个“反向指针”列表记录了“有哪些其他Region的对象引用了本Region内的对象”。这样在回收一个Region时就不用扫描整个堆来判断对象存活只需扫描它的RSet即可极大提升了效率。对象拷贝将Eden区和Survivor区From中存活的对象拷贝到新的Survivor区To或老年代的Region中。这个过程是并行进行的。清空与统计回收掉的Eden和From Survivor Region被清空加入空闲列表。同时G1会统计每个Region的回收时间、存活字节数等数据用于丰富那个“价值”模型。Young GC的目标是高效回收新生代停顿时间相对较短。3.2 并发标记周期Concurrent Marking Cycle这是G1最复杂的部分目的是为了识别出老年代中哪些Region是“高价值”即垃圾多的为后续的Mixed GC做准备。它不会主动回收内存只是一个“侦查”过程。一个完整的周期包括以下几个阶段初始标记STW。仅仅标记从GC Roots直接可达的对象。这个阶段速度极快通常会借一次Young GC的时机“搭便车”完成。根区域扫描并发。扫描在初始标记阶段被标记为“根区域”的Survivor区找出所有指向老年代的引用。这个阶段必须在下次Young GC开始前完成否则就要等待。并发标记并发。从GC Roots开始对堆中对象进行可达性分析标记存活对象。这个阶段耗时最长但与用户线程并发执行不影响应用。重新标记STW。修正并发标记期间因用户线程继续运行而导致变动的标记记录。G1使用了比CMS更高效的SATBSnapshot-At-The-Beginning算法。它假设并发标记开始时堆中对象的关系图是一个快照之后新产生的引用关系即新诞生的对象或新建立的引用会被记录在特殊的队列中在重新标记阶段只需处理这些队列而不需要重新扫描整个堆。清理STW。这个阶段做三件事统计计算每个Region的存活对象比例和回收价值并排序。回收立即回收完全空闲的Region注意这里只回收100%是垃圾的Region。选择根据MaxGCPauseMillis目标和Region价值排序初步选出下次Mixed GC要回收的Region候选集。3.3 混合回收Mixed GC并发标记周期结束后并不会立即触发Mixed GC。G1会等待直到老年代的使用比例超过阈值-XX:InitiatingHeapOccupancyPercent默认45%才会开始一系列Mixed GC。Mixed GC的目标是在用户设定的停顿时间内回收掉价值最高的若干Region这些Region包括一部分新生代Region所有Eden和Survivor和一部分在并发标记周期中被识别为“高垃圾含量”的老年代Region。一次Mixed GC的步骤和Young GC类似也是STW的包括根扫描、处理RSet、对象拷贝等。它与Young GC的关键区别在于回收范围它不仅回收新生代Region还按价值从高到低回收一部分老年代Region。G1会持续进行Mixed GC直到老年代的垃圾比例降低到满意水平或者达到了MaxGCPauseMillis的限制无法选出更多有价值的Region为止。3.4 Full GCG1的失败预案尽管G1设计精良但在极端情况下仍可能失败从而触发一次Serial Old式的Full GC。这通常发生在并发模式失败在并发标记周期尚未完成时老年代就被快速填满没有空间容纳晋升的对象。晋升失败在Young GC或Mixed GC过程中Survivor区或老年代没有足够的空间容纳所有存活对象。巨型对象分配失败无法找到连续的Humongous Region来分配巨型对象。一旦触发Full GCG1会退化为单线程的标记-整理算法停顿时间会非常长这是要极力避免的情况。4. 关键参数调优与避坑指南G1提供了丰富的参数但调优的核心思想是设定合理的停顿时间目标并给予足够的内存和CPU资源。4.1 核心停顿时间目标-XX:MaxGCPauseMillis这是最重要的参数默认200ms。它不是一个硬性保证而是一个软目标。G1会努力达成但为了达成它可能会付出一些代价设置过小如50msG1可能会因为每次回收的Region太少导致回收速度赶不上分配速度最终引发频繁的GC甚至Full GC。同时为了筛选出能在极短时间内回收的高价值RegionG1内部维护成本会上升。设置过大如500msG1每次回收的Region会更多吞吐量可能更好但单次停顿时间变长可能不符合低延迟应用的要求。实操心得不要盲目追求极低的停顿时间。建议先采用默认值200ms通过GC日志观察实际停顿时间分布关注Pause Young (Mixed)的usersys时间。如果应用能接受且没有其他问题就保持。如果实际停顿远小于200ms可以尝试调低如果频繁接近或超过200ms则应调高该值或考虑扩容堆内存。4.2 并发标记触发阈值-XX:InitiatingHeapOccupancyPercent默认45%。当整个堆的使用率超过这个比例时G1会启动并发标记周期。这个参数需要谨慎调整设置过高可能导致并发标记启动太晚在老年代几乎满了才开始大大增加并发模式失败的风险。设置过低会导致并发标记过早、过频繁地启动虽然安全但会占用额外的CPU资源进行“侦查”影响应用吞吐量。实操心得对于对象分配速率高、老年代增长快的应用可以适当调低此值如40%为并发标记留出更多安全余量。监控GC日志中的Concurrent Cycle启动时机确保它在堆使用率达到70-80%前就能完成。4.3 区域大小与巨型对象-XX:G1HeapRegionSize如前所述Region大小影响巨型对象判定。如果你的应用有大量略大于Region大小50%的对象它们会被当作巨型对象处理分配在连续的Humongous Region中。Humongous Region的回收只能在Full GC或并发标记周期的清理阶段进行效率很低。排查技巧通过-XX:PrintGCDetails和-XX:PrintAdaptiveSizePolicy查看GC日志关注Humongous allocations相关的信息。如果发现频繁的巨型对象分配可以考虑适当调大G1HeapRegionSize比如从默认的1M调到2M或4M让这些对象变成普通大对象进入老年代Region参与常规的Mixed GC回收。4.4 记忆集与并发线程数-XX:G1ConcRefinementThreads控制并发更新RSet的线程数。默认值通常够用但在CPU核心数非常多如32核以上且RSet更新压力大的场景可以适当增加此值如设置为CPU核心数的1/4左右以减少Mutator线程自己处理Dirty Card Queue的停顿。-XX:G1ReservePercent默认为堆的10%。这是G1预留的“安全空间”用于在复制存活对象时目标Region空间不足的极端情况。在堆非常大如超过32G且对象晋升模式稳定的应用中可以尝试略微调低此值如5%以增加可用堆空间。但调低会增加晋升失败的风险需密切监控。5. 监控、诊断与常见问题排查用好G1离不开有效的监控和日志分析。5.1 必备的GC日志参数生产环境务必开启以下参数这是诊断的基石-XX:UseG1GC -Xlog:gc*,gcheapdebug,gcergo*trace,gcage*trace:filegc.log:time,uptime,level,tags:filecount10,filesize10MJDK 9 推荐使用新的统一日志框架-Xlog:。这个配置会输出非常详细的GC日志包括每次停顿的原因、持续时间、Region变化、晋升详情、IHOP自适应调整过程等。5.2 常见问题速查表问题现象可能原因排查方向与解决方案频繁的Full GC1. 并发模式失败2. 晋升失败3. 巨型对象分配失败1. 检查InitiatingHeapOccupancyPercent是否设置过高或堆内存是否不足。考虑降低IHOP阈值或增加堆大小-Xmx。2. 检查Young GC后存活对象是否过多。考虑调整-XX:MaxTenuringThreshold晋升年龄阈值或增加Survivor区比例-XX:SurvivorRatio。3. 检查GC日志中的巨型对象分配。考虑调整G1HeapRegionSize或优化代码避免产生过大的短期对象。Young GC停顿时间过长1. RSet处理耗时2. 存活对象过多拷贝耗时1. 观察GC日志中Evacuation Pause阶段的Code Roots Fixup和Other时间。如果过长可能是RSet过大或引用关系复杂。考虑优化数据结构减少跨Region引用。2. 观察Object Copy时间。如果过长说明单次回收的存活对象太多。考虑调小MaxGCPauseMillis让G1每次少回收点Region或者检查是否有内存泄漏导致大量本应死亡的对象存活。Mixed GC回收效果差1. 老年代Region垃圾含量低2. 停顿时间目标限制太紧1. 检查并发标记周期是否正常完成。如果应用产生大量“长寿”对象老年代Region自然垃圾少。这可能是业务特性考虑是否真的需要G1或者优化对象生命周期。2. 适当放宽MaxGCPauseMillis让G1在一次Mixed GC中能选择更多Region进行回收。应用吞吐量明显下降1. 并发标记线程占用CPU2. RSet维护开销大3. GC过于频繁1. 通过系统监控查看在并发标记阶段CPU使用率。如果影响过大可尝试减少并发标记线程数-XX:ConcGCThreads但会延长标记周期。2. 优化代码减少尤其是跨Region的引用写入操作。3. 分析GC频率。如果是分配速率过快考虑优化代码减少对象创建如果是堆太小考虑增加堆内存。停顿时间波动大1. 巨型对象分配导致的零星空闲Region2. IHOP自适应机制正在调整1. 巨型对象分配会占用连续Region可能打乱G1的空间布局。监控并优化。2. G1有自适应的IHOP机制-XX:G1UseAdaptiveIHOP默认开启在应用启动初期会学习对象晋升速率可能导致触发并发标记的时机不稳定。给予应用足够的预热时间或关闭自适应手动设置一个稳定的IHOP值。5.3 一个真实的调优案例电商大促前的准备我曾负责一个核心交易服务堆内存设置32G使用G1默认参数。平时运行平稳但压测模拟大促流量时出现了偶发的、超过500ms的长时间停顿。排查过程分析GC日志发现长时间停顿都发生在Pause Young (Concurrent Start)阶段。这个阶段除了做Young GC还会启动并发标记周期。日志显示此时堆占用率刚好在45%默认IHOP左右。定位根因大促时订单对象创建极快老年代占用率上升很快。在IHOP阈值附近Young GC和并发标记初始阶段STW叠加导致单次停顿时间翻倍。同时由于并发标记启动时机“卡点”在老年代快速填充的压力下有几次险些发生并发模式失败。解决方案降低IHOP阈值将-XX:InitiatingHeapOccupancyPercent从45%下调至35%。目的是让并发标记周期更早启动在堆压力尚小时完成“侦查”工作避免在高压期与Young GC抢时间也留出更多安全空间。增加堆内存从32G增加到40G。这是最直接的缓解方式为老年代增长和GC操作提供缓冲池。微调停顿目标将-XX:MaxGCPauseMillis从200ms调整为250ms。允许单次回收稍微多花一点时间从而每次能回收更多垃圾降低总体GC频率。效果调整后再次压测长时间停顿消失99.9%的GC停顿控制在200ms以内服务稳定性达标。这个案例的关键在于理解了G1各个阶段的工作机制以及参数之间如何相互影响。调优不是孤立地调整一个参数而是根据应用的实际行为模式做出一套联动的、平衡的决策。G1是一个强大的工具但它不是“设置即最优”的魔法。它需要你理解其原理并结合自己应用的特性进行观察和调校。从CMS迁移到G1不仅仅是更换一个启动参数更需要调整监控、告警和性能评估的视角。当你熟悉了它的脾气它就能成为你在应对大内存、低延迟挑战时最可靠的伙伴之一。
返回列表