G1垃圾回收器详解


前言

本来不准备写关于G1垃圾回收器的文章,因为网上介绍的文章真的太多了,写出来容易千篇一律,有抄袭的嫌疑。
但由于最近工作中遇到了G1垃圾回收期的线上优化问题,查找了很多资料,最终还是决定做一个总结,也希望能对大家有所帮助。


一、思考问题

先抛出一些关于G1垃圾回收器的问题,如果你都能回答上来,说明真的吃透了G1垃圾回收器,那么这篇文章你可以跳过了。如果还存在疑问,希望本文能给你解惑。

  1. G1有哪些特点?
  2. G1是分区还是分代?
  3. G1的一个内存单元Region中可以同时包含年轻代和老年代吗?
  4. G1相比CMS有哪些优点?相比ZGC有什么缺点?
  5. G1配置过程中,有哪些重要参数和注意事项?
  6. G1的使用过程中,遇到过哪些问题,怎么解决的?
  7. G1怎么实现目标暂停时间的?
  8. G1垃圾回收的过程中哪些阶段会出现STW?
  9. G1中哪些情况对象会被转移到老年代Old Genetion?
  10. G1中触发GC的时机?
  11. G1有哪些缺点?
  12. G1的适用场景?
  13. G1能否支持上T的大内存?
  14. G1在哪些情况下会出现Full GC?

二、官方文档

官网:https://www.oracle.com/technical-resources/articles/java/g1gc.html

java8下的g1说明
java19下的g1说明
G1垃圾收集器详解

注意
推荐使用的是什么版本的JDK就查看对应版本的官网文档说明,因为不同版本间的一些参数可能会有些细微的不同。由于目前主流还是使用java8,所以本文主要基于java8来对g1垃圾回收器展开介绍。

三、基本介绍

G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术,JDK9 默认就是使用的G1垃圾收集器。

G1其实是Garbage First,表示优先处理那些垃圾多的内存块

Garbage-First (G1)垃圾收集器是一个服务器风格的垃圾收集器,针对具有大内存的多处理器机器,它试图在实现高吞吐量的同时,以较高的概率满足垃圾收集(GC)暂停时间目标。

G1中堆被划分为一组大小相等的堆区域,每个区域有一个连续的虚拟内存范围。G1执行并发全局标记阶段,以确定整个堆中对象的活性。在标记阶段完成后,G1知道哪些区域大部分是空的。它首先收集这些区域,这往往会产生大量的自由空间。这就是为什么这种垃圾收集方法被称为“垃圾优先”
顾名思义,G1将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域。G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数

G1将对象从堆的一个或多个区域复制到堆的单个区域,并在进程中压缩和释放内存。这种疏散是在多处理器上并行执行的,以减少暂停时间并提高吞吐量。因此,对于每次垃圾收集,G1都会持续地减少碎片

需要注意的是,G1并不是一个实时收集器。它满足设定的暂停时间目标,具有较高的概率,但不是绝对确定的。根据以前收集的数据,G1估计在目标时间内可以收集多少个区域。因此,收集器拥有一个相当精确的区域收集成本模型,并使用该模型来确定在暂停时间目标内收集哪些区域以及收集多少个区域。

G1的第一个重点是为运行需要大堆且 GC 延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6 GB 或更大,并且稳定且可预测的暂停时间低于0.5秒
(注意:在Java19推荐10G或者更大的堆内存)

如果应用程序具有以下一个或多个特性,那么当今使用 CMS 或并行压缩运行的应用程序将从切换到 G1中受益。

  • 堆内存超过6G且活跃的数据超过java堆内存的50%
  • 对象分配和晋升的速度非常快
  • 应用程序不希望垃圾回收或内存压缩的暂停时间超过0.5s到1s

G1和CMS有哪些区别?
G1被计划作为并发标记扫描收集器(CMS)的长期替代品,通过比较 G1和 CMS 的差异,可以帮助我们更好的理解G1,并合理的选用。

1、空间压缩:G1采用复制-整理算法,在压缩空间方面有优势,可以避免产生内存空间碎片,而CMS采用标记-清除算法,会产生较多的空间碎片
2、暂停时间的可控性:G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。CMS无法设置目标暂停时间,暂停时间不可控。
3、内存模型方面:G1采用物理分区,逻辑分代,Eden,Survivor,Old区不在是连续的一整块内存,而是又不连续的内存区域Region组成。而CMS中Eden,Survivor,Old区是连续的一整块内存。
4、G1既可以收集年轻代,也可以收集老年代,而CMS只能在老年代使用。

G1和ZGC比较?
最核心的问题是G1未能解决复制-转移过程中准确定位对象地址的问题,无法做到复制-转移过程的并行。而ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。

G1的优缺点
优点:
1、支持较大的内存
2、暂停时间可控
3、压缩空间,避免产生内存碎片
4、简单配置就能达到很好的性能

缺点:
1、记忆集RSet会占用比较大的内存,因此不建议在小内存下使用G1,推荐至少6G
2、对CPU的负载可能会更大一点
3、由于采用复制算法,GC垃圾回收过程对象复制转移会占用较多的内存,更容易出现回收失败(Allocation (Evacuation) Failure)的问题。
4、可能会降低吞吐量
虽然 G1收集器的垃圾收集暂停时间通常要短得多,但应用程序吞吐量也往往略低一些。相当于把一次垃圾回收的工作,分开多次进行执行(主要指老年代),单次暂停的时间虽然更加可控,但是由于每次垃圾回收的空间会更少,总体来说垃圾回收的效率会更低,暂停的总时间会更长,所以吞吐量往往会略低一些。

四、G1的内存模型

G1 是一个既分区也分代的垃圾收集器,这意味着 Java 对象堆(堆)被划分为许多大小相同的区域。在启动时,Java 虚拟机(JVM)设置区域大小。根据堆大小,区域大小可以从1MB 到32MB 不等。目标是不超过2048个地区。伊甸园、幸存者和老一代是这些区域的逻辑集合,并不相邻。

由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1的内存模型:
在这里插入图片描述
说明:
G1是物理分区,逻辑分代

  • 红色区域是年轻代( young generation),包含伊甸区(eden regions,红色不带S区域)和幸存区(survivor regions ,红色带S区域)
  • 浅蓝色区域是老年代(old generation),其中包含跨多个区域组成的大对象区域(Humongous Region,蓝色带H区域)
  • 灰色区域表示空闲区(free regions)

传统的GC内存模型:
传统的垃圾回收器把内存分成三类: Eden(E), Suvivor(S)、Old(O)和持久代。 Eden(E), Suvivor(S)属于年轻代,Old(O)属于老年代,且各区域的内存空间是连续的。

在这里插入图片描述

针对G1的内存模型中的补充说明:

  • 巨型对象Humongous Region
    一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型对象分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)
    G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
    巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(Starts Humongous),相邻连续分区被标记为连续巨型(Continues Humongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。

对于巨型对象,有以下几个点需要注意:

  • 没有被引用的巨型对象会在标记清理阶段或者Full GC时被释放掉。

  • Young GC和Mixed GC阶段都会对巨型对象进行回收

  • 巨型对象永远不会移动,即使在Full GC中

  • 每一个region中都只有一个巨型对象,该region剩余的部分得不到利用,会导致堆碎片化。

  • 如果看到由于大对象分配导致频繁的并发回收,需要把大对象变为普通的对象,建议增大Region size(或者切换到ZGC)。但是增大Region size有一个负面影响就是:减少了可用region的数量。因此,对于这种情况,你需要进行相应的测试,以查看是否实际提高了应用程序的吞吐量或延迟。

  • 在分配巨型对象之前会先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 Full GC。

  • 记忆集合Remember Set (RSet)
    在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。
    注意⚠️:每个区域Region都有一个记忆集RSet,列出了从外部指向该区域的引用。RSet中的信息是实时维护的,也就是每次产生外部引用都会立刻记录到RSet中,而不需要等待GC时才产生。
    在这里插入图片描述

  • 收集集合 (CSet)

CSet收集示意图
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件

五、G1的标记过程

当堆的整体占用足够大时,并发标记开始。默认情况下,它是45%,通过参数InitiatingHeapOccupancyPercent控制。
并发标记(Concurrent Marking)阶段主要是为Mixed GC做准备

G1的标记分为以下几个阶段:
在这里插入图片描述

1、初始标记 Initial marking phase
此阶段标志着从GC根直接到达的所有对象,该阶段依赖于年轻代垃圾收集(young gc),该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。事实上,当达到IHOP阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集,利用年轻代收集的STW时间段,完成初始标记,这种方式称为借道(Piggybacking)

1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]

2、根区域扫描阶段 Root region scanning phase
扫描在初始标记阶段被标识的幸存区域,标记那些被老年代引用的存活对象。此阶段与应用程序(而非 STW)并发运行,必须在下一个 STW 年轻垃圾回收开始之前完成。

在初始标记暂停结束后,年轻代收集也完成了对象复制到Survivor的工作,应用线程开始活跃起来。此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(Root Region Scanning),同时扫描的Suvivor分区也被称为根分区(Root Region)。根分区扫描必须在下一次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合。

1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]

3、并发标记阶段 Concurrent marking phase
G1 GC 在整个堆中查找可访问的(活动的)对象。这个阶段与应用程序同时发生,并且可以被 STW 年轻的垃圾回收中断。

1.364: [GC concurrent-mark-start]
1.645: [GC concurrent-mark-end, 0.2803470 secs]

4、重标记阶段 Remark phase
重新标记(Remark)是最后一个标记阶段。在该阶段中,G1需要一个暂停的时间STW,去处理剩下的SATB日志缓冲区和所有更新,找出所有未被访问的存活对象,同时安全完成存活数据计算。这个阶段也是并行执行的,通过参数-XX:ParallelGCThread可设置GC暂停时可用的GC线程数。同时,引用处理也是重新标记阶段的一部分,所有重度使用引用对象(弱引用、软引用、虚引用、最终引用)的应用都会在引用处理上产生开销,最后还会执行一些类卸载操作。

1.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs] 1.646: [GC ref-proc, 0.0000417 secs] 1.646: [Unloading, 0.0011301 secs], 0.0074056 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

5、清除阶段 Cleanup phase
紧挨着重新标记阶段的清除(Clean)阶段,该阶段是部分并发的。
清除阶段主要执行以下操作:

  • RSet梳理,启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,同时RSet数理也有助于发现无用的引用。参数-XX:+PrintAdaptiveSizePolicy可以开启打印启发式算法决策细节;
  • 整理堆分区,为混合收集周期识别回收收益高(基于释放空间和暂停目标)的老年代分区集合;
  • 识别所有空闲分区,即发现无存活对象的分区。这类分区可在清除阶段直接回收,无需等待下次收集周期。由于这样区域没有存活对象,所以采用并发清空回收。
1.652: [GC cleanup 1213M->1213M(1885M), 0.0030492 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]

如果需要回收一部分没有存活对象的区域,则日志如下:

1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]

在这里插入图片描述
注意:

  1. 在标记过程的最后一个阶段:清除阶段 Cleanup phase,会直接对没有存活对象的分区进行回收,无需等待下次收集周期。
  2. 在初始标记阶段、重标记阶段、清理阶段会出现STW,根区域扫描阶段和并发标记阶段都可以和应用程序并发执行,不会出现STW。

作为对比,CMS的老年代回收采用的是标记-清除算法,其标记过程如下:
在这里插入图片描述

  • 初始标记(CMS Initial Mark) —— 标记GC root能直接关联的对象(短暂STW)
  • 并发标记(CMS Concurrent Mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记
  • 重新标记(CMS Remark)—— 修正并发标记期间因为用户操作导致标记发生表更的对象,采用的incremental update算法,会出现比较多的STW
  • 并发清除(CMS Concurrent Sweep)—— 由于是直接清理,不涉及对象的复制转移,所以阶段可以并发执行。

小结:
CMS在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿,只有初始标记和重新标记会出现STW

这样看好像比G1老年代的标记-复制算法暂停时间更少(因为G1的清理阶段也会出现STW暂停)。其实不然:
1、G1的初始标记阶段是在Young GC的STW过程中同步完成的。
3、重新标记阶段,当堆内存太大时,CMS重新标记的STW时间会逐渐不可控,而G1的重新标记利用RSet采用的SATB算法STW时间非常短暂。
2、CMS会对老年代全区域进行回收,而G1采用预测性算法对老年代Region的回收性价比排序,每次都是在保证暂停时间可控的情况下回收性价比最高的内存Region,所以单次回收的STW时间更加可控。

六、G1的垃圾回收

G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。
在这里插入图片描述

1、G1过程梳理

  1. 应用程序启动时,会首先向服务器申请分配JVM内存,并将申请到的内存划分为许多大小相同的区域Region,这时的区域都是空闲状态 (Free regions)。
  2. 当应用程序开始运行后,会持续产生新的对象,G1内存管理器会分配空闲(Free regions)作为年轻代的伊甸区(eden regions)存放这些新产生的对象。如果新产生的对象大于Region的一半,则直接放入老年区的大对象区域(Humongous Region)。
  3. G1 GC为了匹配软实时(soft real-time)的目标会动态调整年轻代的大小,当年轻代被填满后,就会触发Young GC,Young GC会对整个年轻代和大对象区域(Humongous Region)进行回收。Young GC结束后依然存活的对象,会被疏散evacuation到n(n>=1)个新的Survivor分区,或者是老年代。
  4. 当java heap占用达到 InitiatingHeapOccupancyPercent 定义的阈值之后,在下一个Young GC开始的时候,同时开始进行并发标记(Concurrent Marking)。
  5. 并发标记(Concurrent Marking)和Young GC穿插执行,在Concurrent Marking的过程中可能会出现多次Young-only GC。
  6. 在并发标记(Concurrent Marking)的清理阶段,会直接回收无存活对象的分区。
  7. 当并发标记(Concurrent Marking)结束后,会根据-XX:G1HeapWastePercent=5设置的阈值判断是否需要执行Mixed GC。
  8. 在Mixed GC阶段,会对所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region进行回收。
  9. 如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用Serial old GC(full GC)来收集整个java堆空间。注意G1本身并不提供full GC的。

G1中的垃圾回收主要分为2类:Young GC和Mixed GC
在这里插入图片描述
说明:
1、图中的圆圈表示G1回收过程中的暂停:蓝色圆圈表示Young-only GC导致的暂停,红色圆圈表示Mixed GC导致的暂停,黄色圆圈表示有并发标记导致的暂停。
2、当java heap占用达到 InitiatingHeapOccupancyPercent 定义的阈值之后,下一个Young-only GC收集将也会进行并发标记的初始标记,如图中大蓝色圆圈。
3、Young-only GC和Concurrent Marking阶段可以穿插执行,在Concurrent Marking的过程中可能会出现多次Young-only GC,而Mixed GC只能在Concurrent Marking阶段完成后才能执行。
4、当完成并发标记阶段后,不一定会立刻进行Mixed GC,也可能会进行几次Young-only GC后才会进行Mixed GC。(可能并没有达到G1HeapWastePercent设置的阈值)
5、蓝色圆圈数量多于红色圆圈数量,表示一般情况下,Young-only GC发生的次数往往要大于Mixed GC的次数,这也是G1努力使垃圾回收更加高效。

2、Young GC

年轻代垃圾回收阶段,该阶段也被称为Young-only或Fully Young阶段,会对整个年轻代的区域Region进行回收。

Eden区耗尽的时候就会触发新生代收集,新生代垃圾收集会对整个新生代(E + S)进行回收。

  • 新生代垃圾收集期间,整个应用STW
  • 新生代垃圾收集是由多线程并发执行的
  • 通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • 新生代收集结束后依然存活的对象,会被疏散evacuation到n(n>=1)个新的Survivor分区,或者是老年代。
  • 该阶段会进行大对象区域的回收

Young GC日志示例:
该次Young GC暂停过程中,同时进行了大对象的分配,并完成了并行标记的初始化标记。

2023-02-10T09:43:54.663+0800: 10690912.762: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0274320 secs]
   [Parallel Time: 21.8 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 10690912762.4, Avg: 10690912762.4, Max: 10690912762.6, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 3.2, Avg: 3.5, Max: 3.7, Diff: 0.5, Sum: 14.1]
      [Update RS (ms): Min: 16.6, Avg: 16.7, Max: 17.1, Diff: 0.5, Sum: 66.9]
         [Processed Buffers: Min: 211, Avg: 216.8, Max: 225, Diff: 14, Sum: 867]
      [Scan RS (ms): Min: 0.1, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 1.4]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.5, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 2.9]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 21.3, Avg: 21.4, Max: 21.4, Diff: 0.1, Sum: 85.6]
      [GC Worker End (ms): Min: 10690912783.8, Avg: 10690912783.8, Max: 10690912783.8, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [String Dedup Fixup: 2.4 ms, GC Workers: 4]
      [Queue Fixup (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Table Fixup (ms): Min: 2.1, Avg: 2.2, Max: 2.3, Diff: 0.3, Sum: 8.9]
   [Clear CT: 0.3 ms]
   [Other: 2.9 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.7 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.6 ms]
      [Free CSet: 0.9 ms]
   [Eden: 2170.0M(2454.0M)->0.0B(2452.0M) Survivors: 2048.0K->4096.0K Heap: 3693.3M(4096.0M)->946.1M(4096.0M)]
 [Times: user=0.10 sys=0.00, real=0.03 secs] 

3、Mixed GC

混合垃圾回收阶段,该阶段也被称为Space-reclamation阶段,会选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region进行回收。

  • 整个阶段都是STW
  • 所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region进行回收
  • Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用Serial old GC(full GC)来收集整个GC heap。G1本身并不提供full GC的。
  • 该阶段会进行大对象区域的回收
2023-02-10T09:07:24.661+0800: 10688722.759: [GC pause (G1 Evacuation Pause) (mixed), 0.0105385 secs]
   [Parallel Time: 7.1 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 10688722759.3, Avg: 10688722759.3, Max: 10688722759.4, Diff: 0.0]
      [Ext Root Scanning (ms): Min: 1.6, Avg: 2.0, Max: 2.6, Diff: 1.1, Sum: 7.9]
      [Update RS (ms): Min: 3.9, Avg: 4.4, Max: 4.6, Diff: 0.7, Sum: 17.4]
         [Processed Buffers: Min: 48, Avg: 52.8, Max: 56, Diff: 8, Sum: 211]
      [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.5, Avg: 0.6, Max: 0.7, Diff: 0.3, Sum: 2.3]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 7.0, Avg: 7.0, Max: 7.1, Diff: 0.0, Sum: 28.2]
      [GC Worker End (ms): Min: 10688722766.4, Avg: 10688722766.4, Max: 10688722766.4, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [String Dedup Fixup: 2.3 ms, GC Workers: 4]
      [Queue Fixup (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Table Fixup (ms): Min: 2.2, Avg: 2.2, Max: 2.2, Diff: 0.0, Sum: 8.8]
   [Clear CT: 0.1 ms]
   [Other: 1.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.3 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.1 ms]
      [Free CSet: 0.2 ms]
   [Eden: 204.0M(204.0M)->0.0B(2454.0M) Survivors: 0.0B->2048.0K Heap: 1255.0M(4096.0M)->944.2M(4096.0M)]
 [Times: user=0.04 sys=0.00, real=0.01 secs] 

4、Full GC

作为一种兜底的备份策略,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。

  • 整个过程都是STW
  • G1并不提供full GC的,这个Serial old GC提供的。Full gc是单线程的(在Java 8中)并且非常慢,因此应避免在G1 GC的时候出现这个Full GC

不能觉得用了G1 GC收集器之后,Java heap里面的GC不是Young GC 就Mixed GC,还是存在Full GC。
G1 GC一旦发生了Full GC,就说明当前程序的运行可能出现问题的,需要考虑为什么会Full GC了?

补充:
在这里插入图片描述

G1是如何满足目标暂停时间的?
前提:G1的JVM内存模型:在物理上分区region、逻辑上分代

  1. 在年轻代收集(Young GC)期间,G1 GC 调整年轻代(伊甸园和幸存者)的大小以来匹配软实时(soft real-time)的目标。
  2. 在混合收集(Mixed GC)期间,G1 GC 根据混合垃圾收集的目标数量、堆中每个区域中活动对象的百分比以及总体可接受的堆浪费百分比来调整收集的旧区域的数量,以满足目标暂停时间目标。

七、参数介绍

参数说明
-XX:+UseG1GC使用 G1 收集器
-XX:G1HeapRegionSize=n设置 G1区域Region的大小。范围从1 MB 到32MB之间,目标是根据最小的 Java 堆大小划分出大约2048个区域。
-XX:MaxGCPauseMillis=200设置最长暂停时间目标值,默认是200毫秒
-XX:G1NewSizePercent=5设置年轻代最小值占总堆的百分比,默认值是5%
-XX:G1MaxNewSizePercent=60设置年轻代最大值占总堆的百分比,默认值是java堆的60%
-XX:ParallelGCThreads=n设置STW并行工作的GC线程数,一般推荐设置该值为逻辑处理器的数量,最大是8;如果逻辑处理器大于8,则取逻辑处理器数量的5/8;这适用于大多数情况,除非是较大的SPARC系统,其中的n值可以是逻辑处理器的5/16
-XX:ConcGCThreads=n并发标记阶段,并发执行的线程数,一般n值为并行垃圾回收线程数(ParallelGCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent=45设置触发全局并发标记周期的Java堆内存占用率阈值,默认占用率阈值是整个Java堆的45%
-XX:G1MixedGCLiveThresholdPercent=85老年代Region中存活对象的占比,只有当占比小于此参数的Old Region,才会被选入CSet。这个值越大,说明允许回收的Region中的存活对象越多,可回收的空间就越少,gc效果就越不明显
-XX:G1HeapWastePercent=10设置G1中愿意浪费的堆的百分比,如果可回收region的占比小于该值,G1不会启动并发标记阶段,默认值10%。注意该阈值并不是用来控制Mixed GC的触发条件,这里网上大多数地方说的有误
-XX:G1MixedGCCountTarget=8一次全局并发标记后,最多执行Mixed GC的次数,次数越多,单次回收的老年代的Region个数就越少,暂停也就越短
-XX:G1OldCSetRegionThresholdPercent=10一次Mixed GC过程中老年代Region内存最多能被选入CSet中的占比
-XX:G1ReservePercent=10设置作为空闲空间的预留内存百分比,用来降低目标空间溢出的风险,默认是10%,一般增加或减少百分比时,需要确保也对java堆调整相同的量。

如何解锁VM经验值标志参数?
在Java8中G1的众多参数中,包含3个经验值参数,如果需要调整经验值参数的值,需要先解锁经验值标志。

3个经验值:

-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60
-XX:G1MixedGCLiveThresholdPercent=85

实例:添加UnlockExperimentalVMOptions参数,并调整参数G1NewSizePercent参数的经验值

java -XX:+UnlockExperimentalVMOptions  -XX:G1NewSizePercent=10 -XX:G1MaxNewSizePercent=75 G1test.jar

G1配置注意事项:

  • 避免通过-Xmn 或其他相关选项(如-XX: NewRate)显式设置年轻代的大小,因为年轻代的大小被固定后会导致G1的目标暂停机制失效。
  • 当设置目标暂停时间MaxGCPauseMillis时,需要评估G1 GC 的延迟和应用程序吞吐量之间的取舍,当该值设置较小时,表明您愿意承担垃圾收集开销的增加,从而会导致应用程序吞吐量的降低。
  • Mixed GC的调优:
    1)-XX:InitiatingHeapOccupancyPercent: 设置老年代的内存占比阈值控制Mixed GC的触发时机
    2)-XX:G1HeapWastePercent: 设置G1中愿意浪费的堆的百分比,如果堆内存的使用占比小于该值,G1不会启动Mixed GC,默认值10%,主要用来控制并发标记的触发时机。
    3)-XX:G1MixedGCLiveThresholdPercent:设置老年代Region进入CSet的活跃对象占比阈值,避免活跃对象占比过高的Region进入CSet
    3)-XX:G1MixedGCCountTarget and -XX:G1OldCSetRegionThresholdPercent: 主要是为了控制单次Mixed GC中Region的个数,CSet中Region的个数越多,GC过程中暂停时间越长。

八、分析各阶段触发时机

根据GC日志分析Young GC的触发时机

通过执行命令grep -a30 'young' jvm.gc.0.current查看Young GC的相关日志信息:

1、Eden区填满触发Young GC
在这里插入图片描述
2、Survivor区晋升空间不足,触发Young GC
这种情况有点强行解释的嫌疑,Eden区明明还剩余很多空间,但是却显示内存耗尽。
在这里插入图片描述
3、巨型对象分配,触发Young GC
可以明显看到,Eden区并没有被填满,但还是触发了Young GC,但根据G1 Humongous Allocation可以看出是巨型对象分配触发Young GC。
在这里插入图片描述

根据GC日志分析并发标记的触发时机

通过观察GC日志验证XX:G1HeapWastePercent=10的并发标记触发条件:
当Young GC结束后,堆内存占用情况430/4096 > 10%,满足并发标记阶段的触发条件,开启并发标记阶段,为后续的Mixed GC做准备

参数-XX:G1HeapWastePercent是用来控制并发阶段的发起时机的阈值,而非Mixed GC开始的阈值

grep -b2 'concurrent-root-region-scan-start' jvm.gc.0.current

在这里插入图片描述

注意⚠️:
关于-XX:G1HeapWastePercent很多地方说默认值是5,但是经过Oracle官网和日志分析的最终结论是-XX:G1HeapWastePercent的默认值是10。注意:这里是在java8下验证。
在这里插入图片描述

根据GC日志分析Mixed GC的触发时机

官网中对-XX:InitiatingHeapOccupancyPercent=45的说明都是当堆的占用达到45%后,触发并发标记阶段,但是上面已经介绍-XX:G1HeapWastePercent才是用来控制并发标记的。
而且通过日志观察发生Mixed GC的时候,显然堆使用占比显然并没有超过45%.
969 / 4095 < 45%
该参数其实指的是老年代的使用占比: 969/ (4096 - 2442-14)= 58% > 45%
在这里插入图片描述
在java19的文档中也可以发现,参数-XX:InitiatingHeapOccupancyPercent的介绍已经是老年代的使用占比45%。但这里还是将该参数认为是并发标阶段的开始阈值。
在这里插入图片描述

九、典型问题

1、疏散失败(Evacuation Failure)

当没有更多的空闲region被提升到老一代或者复制到幸存空间时,并且由于堆已经达到最大值,堆不能扩展,从而发生Evacuation Failure,这时G1 的GC已经无能为力,只能使用通过Serial old GC进行Full GC来收集整个java堆空间,这个过程就是转移失败(Evacuation Failure)。

Young GC 疏散暂停(Evacuation Pause)过程出现内存耗尽的对应日志:
在这里插入图片描述
注意:G1 Evacuation Pause指的是G1垃圾回收过程中存活对象的复制-转移阶段,被称为‘疏散暂停’阶段。

解决方案:

  • 如果有大量“空间耗尽(to-space exhausted)”或“空间溢出(to-space overflow)”GC事件,则增加-XX:G1ReservePercent以增加“to-space”的预留内存量,默认值是Java堆的10%。注意:G1 GC将此值限制在50%以内。
  • 通过减少 -XX: InitiatingHeapOccupancyPercent 的值来更早地启动并发标记周期,来及时回收不包含活跃对象的区域,同时促使Mixed GC更快发生。
  • 增加选项-XX:ConcGCThreads的值以增加并行标记线程的数量,减少并行标记阶段的耗时。

2、大对象分配(Humongous Allocation)

Young GC过程中大对象分配时出现内存耗尽的对应日志:
在这里插入图片描述
原因分析:
出现大对象分配导致的内存耗尽问题,一般是老年代剩余的Region中已经不能够找到一组连续的区域分配给新的巨型对象。

解决方案:

  • 首先要分析程序中频繁产生巨型对象是否正常,比常见的如查询sql没有加limit导致返回的数据量过大,而不是直接增加Region的大小。
  • 通过-XX: G1HeapRegionSize 选项来增加内存区域Region的大小,提升Region对象的判断标准,以减少巨大对象的数量。
  • 增加堆java的大小使得有更多的空间来存放巨型对象
  • 通过-XX:G1MaxNewSizePercent降低年轻代Region的占比,给老年代预留更多的空间,从而给巨型对象提供给多的内存空间。
  • 通过-XX:G1ReservePercent增加“to-space”的预留内存量

一般在疏散阶段(Evacuation Pause)和大对象分配(Humongous Allocation)会比较容易出现“空间耗尽(to-space exhausted)”或“空间溢出(to-space overflow)”的GC事件,导致出现转移失败(Evacuation Failure) ,进而引发Full GC 从而导致GC的暂停时间超过G1的设置的目标暂停时间。
所以我们要尽量避免出现转移失败(Evacuation Failure)。
在这里插入图片描述

3、Young GC花费时间太长

通常Young GC的耗时与年轻代的大小成正比,具体地说,是需要复制的集合集中的活跃对象的数量。

如果Young GC中CSet的疏散阶段(Evacuate Collection Set phase)需要很长时间,尤其是其中的对象复制-转移,可以通过降低-XX:G1NewSizePercent的值,降低年轻代的最小尺寸,从而降低停顿时间。

还可以使用-XX:G1MaxNewSizePercent降低年轻代的最大占比,从而减少Young GC暂停期间需要处理的对象数量。

4、Mixed GC耗时太长

  • 通过降低-XX:InitiatingHeapOccupancyPercent的值,来调低并发标记阶段开始的阈值,让并发标记阶段更早触发,只有并发标记完成才能开始执行Mixed GC。
  • 通过调节-XX:G1MixedGCCountTarget-XX:G1OldCSetRegionThresholdPercent参数,降低单次回收的Region数量,减少暂停时间。
  • 通过调节-XX:G1MixedGCLiveThresholdPercent的值,避免活跃对象占比过高的Region进入CSet。因为活的对象越多,region中可回收的空间就越少,暂停时间就越长,gc效果就越不明显。
  • 通过调节-XX:G1HeapWastePercent的值,设置愿意浪费的堆的百分比。只有垃圾占比大于此参数,才会发生Mixed GC,该值越小,会越早触发Mixed GC。

总结

本来主要对G1垃圾回收器的相关特性和实现机制做了详细介绍。
1、首先G1的内存模型进行了说明:物理上分区,逻辑上分代,年轻代和老年代的区域不是连续的内存空间,而是由分散的大小相同的内存块Region组成,G1 GC会通过动态条件年轻代区域Region的数量以来匹配软实时(soft real-time)的目标。
2、详解介绍了G1 GC中的完整的垃圾回收流程以及Young GC、并发标记和Mixed GC的实现细节。
3、介绍了G1 GC的优缺点以及选用场景。
4、对G1的核心参数做了相关介绍,并针对G1的重点问题提供了参数优化建议。

  • 11
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

斗者_2013

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值