G1 GC垃圾收集流程

概念

从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为:

  • 在新生代,G1 采用的仍然是并行的复制算法,所以同样会发生 Stop-The-World 的暂停。
  • 在老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,并且不是整体性的整理,而是增量进行的。

G1内存空间划分

  • region

    G1将堆分成了若干Region。Region的大小可以通过G1HeapRegionSize参数进行设置,其必须是2的幂,范围允许为1Mb到32Mb。JVM的会基于堆内存的初始值和最大值的平均数计算分区的尺寸,平均的堆尺寸会分出约2000个Region。分区大小一旦设置,则启动之后不会再变化
    G1 GC垃圾收集流程_第1张图片
    image.png
  • Eden regions(年轻代-Eden区)

  • Survivor regions(年轻代-Survivor区)

  • Old regions(老年代)

  • Humongous regions(巨型对象区域)- 存放超过region 一半大小的对象

  • Free resgions(未分配区域,也会叫做可用分区)-上图中空白的区域

GC过程

Evacuation Pause
拷贝的过程称为Evacuation(译注:意为疏散)

Young GC(Evacuation Pause: Fully Young)

  • 垃圾回收的过程就是 Allocated->eden; eden -> survivor; survivor -> survivor; survivor -> old;
G1 GC垃圾收集流程_第2张图片
图片
  • 可以看到,这里有 eden,survivor,old 还有个 free region。
G1 GC垃圾收集流程_第3张图片
图片
  • 橙色就是活着的对象
G1 GC垃圾收集流程_第4张图片
图片
  • G1会把橙色对象拷贝到free region
    当拷贝完毕,free region 就会晋升为 survivor region,以前的 eden 就被释放了

注意的是 正常来说,大部分在 Young 的对象都不会存活很长时间
如果 Young gc 中,花费了大量的时(大部分在 Young 的对象都不会存活很长时间),你可能需要调整一下 Young 区域占比。来降低 Young 对象的拷贝时间。
-XX:G1NewSizePercent (默认:5) Young region 最小值
-XX:G1MaxNewSizePercent (默认: 60) Young region 最大值

G1 GC垃圾收集流程_第5张图片
图片

YoungGC 触发时机
在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次young gc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。因为YoungGC会进行根扫描,所以会stop the world

YoungGC的回收过程如下
1.根扫描root scan,跟CMS类似,Stop the world,扫描GC Roots对象。
2.处理Dirty card,更新RSet.
3.扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
4.拷贝扫描出的存活的对象到survivor2/old区
5.处理引用队列,软引用,弱引用,虚引用

Mixed GC(Evacuation Pause: Mixed)

  • Mixed gc 会选取所有的 Young region + 收益高的若干个 Old region。
G1 GC垃圾收集流程_第6张图片
图片
G1 GC垃圾收集流程_第7张图片
图片
G1 GC垃圾收集流程_第8张图片
图片

值得高兴的情况是, cleanup可以释放老年代的整个regions. 但是并不是每次都是这样, 当并发标记成功结束后,G1会预定一个混合的收集来收集年轻代regions的垃圾,也会在收集集合中加入一部分老年代的regions.

一个混合的Evacuation Pause并不总是并发标记阶段结束后立即开始. 有一系列的规则和启发式算法来决定这个. 比如, 可以释放掉老年代的一大部分空间, 那么就没必要做这个了.

因此,就是在并发标记结束和混合Evacuation Pause之间加入很多fully-young的Evacuation Pause.

具体放入收集集合的老年代区的region,以及它们被加入的顺序都基于一系列的规则选择出来的. 这些规则包括:应用设定的软的实时性能指标, 存活统计以及并发标记阶段垃圾回收的效率, 还有一系列可配的JVM 选项. 混合式收集大体上与我们前面看到fully-young相同, 但这次我们讲到新的对象remembered sets.

remembered sets 用来支持在不同heap regions上的独立收集. 比如当收集region A,B,C, 我们只需要知道从region D和E中是否有引用到它们来决定它们的存活性.因为遍历整个堆会消耗很久的时间并且打破了我们增量收集的意义, 所以在G1中也采用了与在其他算法中采用Card Table来独立收集年轻代区域类似的优化算法, 叫做remember sets.

如下图所示, 每个region都有一个RSet保存从其他region到这个region中对象的引用. 这些对象会被当做额外的GC roots. 注意在并发标记阶段, 老年代被认为是垃圾的对象会被忽略, 即便有外部对象还在引用它们, 因为它们的对象也会被当做垃圾.

接下来发生的与其他收集器类似:多个并行的GC线程会找出哪些是存活的哪些是垃圾. 最后, 所有存活对象会被移动到survivor区(如有必要创建新的).所有的空region会被释放后被用来存放对象. [[图片上传失败.
为了在应用程序运行期间维护RSets, 任何时候对域的更新都会触发一个Post-Write屏障. 如果关联的引用是跨region的, 比如从一个region到另一个region,一个对应的记录也会在目标region的RSet中添加. 将记录(cards)加入到RSet是异步的并应用了很多优化.简单来说它用Write屏障来将脏记录放到本地buffer中, 一个特殊的GC线程会选择这些记录,然后传播信息给其他region的RSet.

Young GC发生的时机大家都知道,那什么时候发生Mixed GC呢?其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。

  • G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
    指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占Java 堆的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率。

gc_handbook_zh

global concurrent marking(oracle叫标记周期)

  • 初始标记(initial mark,STW,piggy-backed on an Evacuation Pause)。它标记了从GC Root开始直接可达的对象。
    [GC pause (G1 Evacuation Pause) (young) (initial-mark)
    initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的,因此initial-mark 在global concurrent marking中不会占用额外的暂停时间,在通常多次young GC 后发生一次global concurrent marking
  • Root Region Scan Root区扫描 Root Region Scan 在这个阶段, 会标记所有的从所谓的根区可以到达的对象. 比如那些不是空的而且我们必须在标记阶段的中间结束收集的Regions. 因为在并发标记阶段来移动对象会导致麻烦, 所以这个阶段必须在下一个Evacuation Pause之前完成. 如果Evacuation Pause必须提前开始, 它必须提前请求中断root region scan, 然后等root region scan完成. 在当前实现中, root regions是survivor regions:他们是年轻代的第一部分,而且可以再下次Evacuation Pause得到很好的收
  • 并发标记(Concurrent Marking)。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
    该阶段采用三色标记算法
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Cleanup,STW)。清除空Region(没有存活对象的),加入到free list。

第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
第四阶段Cleanup只是回收了没有任何存活对象的Region,所以它并不需要STW

整个GC 周期

G1 GC垃圾收集流程_第9张图片
image.png

G1 有两个阶段,它会在这两个阶段往返,分别是 Young-onlySpace Reclamation

  • Young-only 包含一系列逐渐填满 old gen 的 gc

  • Space Reclamation G1 会递进地回收 old gen 的空间,同时也处理 Young region

图是来自 oracle 上对 gc 周期的描述,实心圆都表示一次 GC 停顿

  • 蓝色 Young-only

  • 黄色 标记过程的停顿

  • 红色 Mixed gc 停顿

这里简单说一下 humongous 对象的处理,上面没有提到

  • humongous 对象在G1中是被特殊对待的,G1 只决定它们是否生存,回收他们占用的空间,从不会移动它们

  • Young-Only 阶段,humongous regions 可能会被回收

  • Space-Reclamation(mix gc),humongous regions 可能会被回收

  • G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。 该参数的默认值是85%。规定只有存活对象低于85%的Region才可以被回收
  • G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
    设置标记周期完成后,对存活数据上限为 G1MixedGCLIveThresholdPercent 的region执行混合垃圾回收的目标次数。默认值是 8 次混合垃圾回收,意味着要在8次以内回收完所有的 old region
    换句话说,如果你有 800 个 old region, 那么一次 mixed gc 最大会回收 100 个 old region
  • G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量
    设置混合垃圾回收期间要回收的最大旧区域数。默认值是 Java 堆的 10%

详细解释下:G1MixedGCLiveThresholdPercent

  • 为什么要有这个规定?
    因为G1是基于复制算法来处理垃圾对象的,把存活对象复制到另一个空闲的Region,剩下的就是垃圾对象了,然后就可以直接回收整个Region。试想下,如果一个Region中的存活对象大于85%,把这85%对象都复制到另一个空闲的Region,成本还是很高的,而且Region改变也不大,所以回收价值不高,因此可以让G1把有限的时间拿去回收那些回收价值更大的Region,以此把GC停顿时间控制在范围内。
    调整该参数有什么影响?
  • 个人理解,如果调大该参数,那么极端情况下存活对象很多,Region剩余的空间就不多,如果此时要分配一个对象,但是所有的Region的剩余空间都很小,分配不下,就会触发Serial Old垃圾收集器,进入耗时更长的STW状态,影响应用程序性能。如果调小该参数,可以让Region的回收效率更高,可以被选择回收的Region更多,此时可能有一些保存着短期存活对象的Region被纳入回收范围,导致这些短期对象侥幸逃过几次垃圾收集,晋升老年代,这就会提前引起老年代GC了。

除了以上的参数,G1 GC相关的其他主要的参数有:
参数 含义
-XX:G1HeapRegionSize=n 设置Region大小,并非最终值
-XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms,不是硬性条件
-XX:G1NewSizePercent 新生代最小值,默认值5%
-XX:G1MaxNewSizePercent 新生代最大值,默认值60%
-XX:ParallelGCThreads STW期间,并行GC线程数
-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数
-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous
-XX:G1ReservePercent,通过-XX:G1ReservePercent指定G1为分配担保预留的空间比例,默认10%。也就是老年代会预留10%的空间来给新生代的对象晋升,如果经常发生新生代晋升失败而导致Full GC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间
-XX:G1HeapWastePercent
通过-XX:G1HeapWastePercent指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整对的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率

其他参数

  • -XX:-ResizePlaB
    PLAB全称为Promotion Local Allocation Buffers,它被用于年轻代回收。PLAB的作用是避免多线程竞争相同的数据,处理方式是每个线程拥有独立的PLAB,用于针对幸存者和老年空间。当应用开启的线程较多时,最好使用-XX:-ResizePlaB来关闭PLAB()的大小调整,以避免大量的线程通信所导致的性能下降
  • -XX:+AlwaysPreTouch
    JAVA进程启动的时候,虽然我们可以为JVM指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配,这样会造成以下问题。
    1、GC的时候,新生代的对象要晋升到老年代的时候,需要内存,这个时候操作系统才真正分配内存,这样就会加大young gc的停顿时间;
    2、可能存在内存碎片的问题。

因此AlwaysPreTouch,JVM就会先访问所有分配给它的内存,让操作系统把内存真正的分配给JVM.后续JVM就可以顺畅的访问内存了

关于AlwaysPreTouch找了一些资料,这个参数属于比较偏门的优化项
JAVA进程启动的时候,虽然我们可以为JVM指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配,这样会造成以下问题:

  1. 第1次YGC之前Eden区分配对象的速度较慢;

  2. YGC的时候,Young区的对象要晋升到Old区的时候,这个时候需要操作系统真正分配内存,这样就会加大YGC的停顿时间;

启动时间

配置-XX:+AlwaysPreTouch参数可以优化这个问题,不过这个参数也有副作用,它会影响启动时间,那影响到底有多大呢?请接着往下看。


G1 GC垃圾收集流程_第10张图片
image.png

并行PreTouch

配置这个参数后这么耗时其中一个原因是,这个特性在JDK8版本以前都不是并行处理的,到了JDK9才是并行。可以戳链接Parallelize Memory Pretouch: https://bugs.openjdk.java.net/browse/JDK-815795

根本原因

配置-XX:+AlwaysPreTouch参数后,JVM进程启动时间慢了几个数量级的根本原因呢?

在没有配置-XX:+AlwaysPreTouch参数即默认情况下,JVM参数-Xms申明的堆只是在虚拟内存中分配,而不是在物理内存中分配:它被以一种内部数据结构的形式记录,从而避免被其他进程使用这些内存。这些内存页直到被访问时,才会在物理内存中分配。当JVM需要内存的时候,操作系统将根据需要分配内存页。

配置-XX:+AlwaysPreTouch参数后,JVM将-Xms指定的堆内存中每个字节都写入’0’,这样的话,除了在虚拟内存中以内部数据结构保留之外,还会在物理内存中分配。并且由于touch这个行为是单线程的,因此它将会让JVM进程启动变慢。所以,要么选择减少接下来对每个缓存页的第一次访问时间,要么选择减少JVM进程启动时间,这是一种trade-off。

调优

  • 避免Full GC

在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?

并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:

  • 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。

  • 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。

  • 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

参考

  1. Getting Started with the G1 Garbage Collector 官方文档-翻译
  2. Getting Started with the G1 Garbage Collector 官方文档
  3. Understanding G1 GC Logs
  4. G1 GC log的解读-翻译
  5. gc调优-小米

分享文档
Java Hotspot G1 GC的一些关键技术- 美团
Plumbr Handbook Java Garbage Collection.pdf 翻译
G1GC 概念与性能调优- oppo
hbase G1

https://www.jianshu.com/p/5d4e319582f7

你可能感兴趣的:(G1 GC垃圾收集流程)