高并发系统必看!G1如何让亿级JVM吞吐量提升300%?

开发 前端
G1(Garbage-First)垃圾回收器应运而生,它以“可控的停顿时间”为核心设计目标,通过分区(Region)模型和智能回收策略,实现了低延迟与高吞吐的平衡。

在 Java 应用的运行中,垃圾回收(GC)是保障内存安全的核心机制,但传统 GC 如 CMS 和 Parallel GC 常面临两大痛点:不可控的停顿时间和内存碎片问题。

例如,CMS 的“并发模式失败”可能导致长达数秒的 Full GC 停顿,而大内存场景下频繁的 Young GC 和 Mixed GC 可能拖累吞吐量。

G1(Garbage-First)垃圾回收器应运而生,它以“可控的停顿时间”为核心设计目标,通过分区(Region)模型和智能回收策略,实现了低延迟与高吞吐的平衡。

本文将从设计思想、核心机制到实战调优,带你深入理解 G1 如何解决传统 GC 的难题。

G1 分区模型

Region 分区模型

G1 将堆内存划分为多个等大小的 Region(默认 1MB-32MB),每个 Region 可以是 Eden、Survivor 或 Old 区,默认是将堆内存按照 2048 份均分。

这种灵活的分区方式打破了传统代际的物理隔离,允许动态调整各代内存占比。

Region 的角色(年轻代、老年代或大对象区)是动态分配的。例如,一个 Region 可能初始作为 Eden 区,回收后被标记为 Survivor 区,后续可能转为老年代区。

大对象(Humongous 对象):若对象大小超过 Region 的 50%,则分配到连续的 Humongous Region。此类对象回收需特殊处理,若空间不足可能触发 Full GC。

例如,当老年代占用过高时,G1 会优先回收垃圾最多的 Region,而非全堆扫描

跨代引用的智能追踪

卡表(Card Table):记录跨 Region 的引用关系。例如,老年代对象引用年轻代对象时,对应的卡表条目会被标记为“脏卡”。

记忆集(RSet):每个 Region 维护一个 RSet,存储其他 Region 对其内部对象的引用。通过 RSet 快速定位跨 Region 引用,避免全堆扫描。

写屏障(Write Barrier):在对象引用修改时触发,更新卡表和 RSet。例如,当老年代对象引用新生代对象时,写屏障会记录该引用。

混合回收(Mixed GC)

混合回收是 G1 的精髓:在一次回收中,同时处理年轻代和老年代的 Region

通过计算回收收益(垃圾量/耗时),G1 选择性价比最高的 Region 集合(Collection Set),在用户设定的最大停顿时间内(如 200ms)完成回收。

在逻辑上,G1 分为年轻代和老年代,但它的年轻代和老年代比例,并不是那么“固定”,为了达到 MaxGCPauseMillis 所规定的效果,G1 会自动调整两者之间的比例。

如果你强行使用 -Xmn 或者 -XX:NewRatio 去设定它们的比例的话,我们给 G1 设定的这个目标将会失效。

G1 的回收过程主要分为 3 类:

  1. G1“年轻代”的垃圾回收,同样叫 Minor GC,这个过程和我们前面描述的类似,发生时机就是 Eden 区满的时候。
  2. 老年代的垃圾收集,严格上来说其实不算是收集,它是一个“并发标记”的过程,顺便清理了一点点对象。
  3. 真正的清理,发生在“混合模式”,它不止清理年轻代,还会将老年代的一部分区域进行清理。

年轻代回收(Young GC)

Chaya:年轻代回收触发流程是什么?

Eden 区占满时触发,仅回收年轻代 Region。回收流程如下所示:

  • 根扫描:标记 GC Roots 直接可达的对象(如栈帧局部变量、静态变量)。
  • RSet 处理:通过脏卡队列更新 RSet,将老年代对年轻代的引用加入 GC Roots。
  • 复制存活对象:将 Eden 和 Survivor 区的存活对象复制到新 Survivor 区,年龄达阈值(默认 15)则晋升老年代。
  • 这个过程通常是 Stop-The-World(STW)的,即在回收过程中,应用程序的其他线程会被暂停。

混合回收(Mixed GC)

Chaya:混合回收的触发条件是什么?

多次回收之后,会出现很多 Old 老年代区,此时总堆占有率达到阈值(默认 45%)时会触发混合回收 MixedGC。混合回收会回收 整个年轻代 + 部分老年代。

回收过程如下:

  • 初始标记(STW):标记 GC Roots 直接可达对象,耗时短。
  • 并发标记:与应用线程并行,遍历堆标记存活对象,使用三色标记法(黑、灰、白)避免漏标。
  • 重新(最终)标记(STW):处理并发标记期间引用变化(通过 SATB 算法保证一致性),修正标记结果。
  • 筛选回收:根据 Region 的回收价值(垃圾占比与回收时间)选择 Region 集合(Collection Set),复制存活对象并清理。

注意:当混合回收无法快速释放足够空间时触发 Full GC(如大对象分配失败),采用单线程标记-整理算法,导致长停顿。

初始标记

初始标记会暂停所有用户线程,只标记从 GC Root 可直达的对象,所以停顿时间不会太长。

采用三色标记法进行标记,三色标记法在原有双色标记(黑也就是 1 代表存活,白 0 代表可回收)增加了一种灰色。

三色标记法

  • 黑色:对象及其引用均完成标记。
  • 灰色:对象已标记,但引用未完全处理。
  • 白色:未标记或待回收对象。
  • 漏标问题解决:通过 SATB(Snapshot-At-The-Beginning)机制,记录并发标记开始时的对象快照,确保标记一致性

并发标记

默认线程数为ParallelGCThreads的 1/4(通过-XX:ConcGCThreads调整),减少应用线程阻塞。

允许系统程序的运行,同时进行"GC Roots"追踪,追踪所有存活对象(间接引用的对象)。该阶段很耗时,因为要追踪全部的存活对象。但是是并发运行,对系统影响不大。

GC 开始前所有对象都是白色,GC 一开始所有根能够直达的对象被压到栈中,待搜索,此时颜色是灰色。

然后灰色对象依次从栈中取出搜索子对象,子对象也会被涂为灰色,入栈。

当其所有的子对象都涂为灰色之后该对象被涂为黑色。

当 GC 结束之后灰色对象将全部没了,剩下黑色的为存活对象,白色的为垃圾。

需要注意的是:由于用户线程可能同时在修改对象的引用关系,就会出现错标的情况。

Chaya:那咋办呢?

G1 为了解决这个问题,使用了SATB 技术(Snapshot At The Beginning, 初始快照)。

在并发标记开始时,G1 会创建一个堆内存的快照,记录所有存活对象的初始状态。

在最终标记阶段,系统会处理并发期间新增的引用变化,通过写前屏障(Write Barrier)记录这些变化,确保新对象不被错误回收。

最终标记

触发时机:在并发标记(Concurrent Marking)完成后,G1 需要暂停所有应用线程(STW),以处理并发标记期间遗漏的引用变化,确保标记结果的准确性。

核心目标:修正并发标记阶段因应用线程并发执行导致的对象引用变化(如新对象创建或引用更新),并生成最终的存活对象快照。

处理漏标对象:通过遍历卡表(Card Table)中的“脏页”(记录引用修改的区域),重新扫描这些区域的对象,修正标记状态

筛选回收

最终标记完成后,G1 根据停顿时间目标(MaxGCPauseMillis)和Region 回收价值,选择最合适的区域进行回收。

优先回收垃圾比例高(存活对象少)的 Region,以最小化回收时间并最大化内存释放效率。

根据每个 Region 的存活对象数量和回收时间成本计算“回收价值”,优先选择存活率低、回收效率高的 Region 组成回收集(Collection Set, CSet)。

标记-复制算法:将存活对象从回收集的 Region 复制到空闲 Region,同时整理内存以减少碎片。

主要步骤如下所示:

构建回收集(CSet):

  • 根据 Region 的存活对象比例和用户设定的停顿时间目标(如-XX:MaxGCPauseMillis),动态选择需要回收的 Region。
  • 通常包括所有年轻代 Region(Eden/Survivor)和部分老年代 Region(混合收集模式)

并行迁移存活对象:

  • 暂停应用线程(STW),启动多个 GC 线程并行执行。
  • 将回收集内的存活对象复制到空闲 Region(如 Survivor 区或 Old 区的新 Region),并更新对象引用指针。

清理与释放内存:

  • 清空原 Region 的所有内容,将其标记为“空闲区域”。
  • 更新Remembered Set(RSet)和卡表,记录跨 Region 引用的变化。

调优策略与参数

案例一:年轻代配置

案例:某线上服务因误设-Xmn256m覆盖 G1 的自动调节,导致 Eden 区过小(仅 256MB),频繁触发 Young GC(600+次/压测),响应时间激增。

解决方案:删除-Xmn参数,由 G1 根据G1NewSizePercent(默认 5%)和G1MaxNewSizePercent(默认 60%)动态调整新生代大小,GC 时间从 25 秒降至 1 秒内。

案例二:老年代“拥堵治理”

动态年龄判定:若 Survivor 区使用超过 50%(TargetSurvivorRatio默认值),对象会直接晋升老年代。需通过增大 Survivor 区或降低晋升阈值(MaxTenuringThreshold),避免过早“占道”。

混合回收触发阈值:默认InitiatingHeapOccupancyPercent=45%(老年代占比),高并发场景可适度调低以提前回收,避免 Full GC。

大对象“专车配送”

大对象(超过 Region 50%)直接进入老年代,类似超重订单需特殊车辆处理。通过G1HeapRegionSize调整 Region 大小(如 32MB),或设置PretenureSizeThreshold控制大对象阈值,减少内存碎片。

Full GC 的应急处理

内存不足:堆内存过小或老年代晋升过快,需检查-Xmx/-Xms是否一致(建议设为物理内存 75%-80%)。

并发失败:若 Mixed GC 无法及时回收,触发 Full GC,需优化MaxGCPauseMillis或降低InitiatingHeapOccupancyPercent。

启用 GC 日志:-XX:+PrintGCDetails -Xloggc:/path/gc.log,关注Full GC关键字及耗时。

关键调优参数

  • -XX:MaxGCPauseMillis:设定最大停顿时间(默认 200ms),G1 根据此目标动态调整回收 Region 数量.
  • -XX:G1HeapRegionSize:手动指定 Region 大小(需为 2 的幂次方)。
  • -XX:G1MixedGCCountTarget:控制混合回收次数(默认 8 次),分批次回收老年代 Region 以减少单次停顿。
  • -XX:G1ReservePercent:预留堆内存(默认 10%)防止晋升失败。

总结

G1 是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的 GC 暂停目标,就能得到不错的性能;同时,我们也看到 G1 对内存空间的浪费较高,但通过首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。

虽然 G1 也有类似 CMS 的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。

  • G1 的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1 并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时 G1 可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  • G1 采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此 G1 天然就是一种压缩方案(局部压缩);
  • G1 虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的 survivor(to space)堆做复制准备。G1 只有逻辑上的分代概念,或者说每个分区都可能随 G1 的运行在不同代之间前后切换;
  • G1 的收集都是 STW 的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
责任编辑:姜华 来源: 码哥跳动
相关推荐

2024-05-23 16:41:40

2024-11-08 13:36:09

2024-06-06 16:15:00

2023-11-07 15:11:46

Kafka技巧

2023-08-03 14:18:29

Rust阻塞函数

2023-02-09 08:57:11

Callable异步java

2020-09-01 07:49:14

JVM流量系统

2022-09-15 08:10:18

多线程场景QPS

2010-04-14 16:02:09

IDF

2024-03-20 10:39:52

微软Garnet缓存存储

2021-06-16 07:05:02

gRPC 网关HTTP

2013-04-19 09:45:20

AMPLabHadoopHDFS

2024-09-12 15:24:29

2024-09-09 14:12:38

2023-11-03 18:23:34

虚拟线程服务器

2021-12-26 00:03:27

响应式编程异步

2024-12-13 13:58:53

2024-01-19 13:42:00

模型训练

2024-09-14 11:31:27

@AsyncSpring异步

2019-07-26 15:41:27

程序员技能开发者
点赞
收藏

51CTO技术栈公众号