0
点赞
收藏
分享

微信扫一扫

Java GC G1 详解


多看看JDK的JEP(JDK Enhancement Proposal),少看网上的垃圾文章。
通过JEP你可以知道,某个特性产生的背景是什么,是怎么解决的问题。

Java GC G1 详解_垃圾回收

G1(Garbage-First)回收器是在JDK1.7中正式使用的全新垃圾回收器,G1拥有独特的垃圾回收策略,从分代上看,G1依然属于分代垃圾回收器,它会区分年代和老年代,依然有eden和survivor区,但从堆的结构上看,它并不要求整个eden区、年清代或者老年代都连续。它使用了全新的分区算法。其特点如下:

  • 并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
  • 并发性:G1拥有与应用程序交替执行的能力,因此一般来说,不会在整个回收期间完全阻塞应用程序。
  • 分代GC:与之前回收器不同,其他回收器,它们要么工作在年轻代要么工作在老年代。G1可以同时兼顾年轻代与老年代。
  • 空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单的标记清除,在若干次GC后CMS必须进行一次碎片整理,G1在每次回收时都会有效的复制对象,减少空间碎片。
  • 可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收范围,因此对于全局停顿也能得到更好的控制。

一、G1的内存划分和主要收集过程

G1收集回收器将堆进行分区,划分为一个个的区域,每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生一次停顿时间。
G1的收集过程可能有4个阶段:

  • 新生代GC
  • 并发标记周期
  • 混合收集
  • (如果需要)进行Full GC。

二、G1的新生代GC

新生代GC的主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。新生代GC收集前后的堆数据如图5.6所示,其中E表示eden区,S表示survivor区,O表示老年代。可以看到,新生代GC只处理eden和survivor区,回收后,所有的eden区都应该被清空,而survivor区会被收集一部分数据,但是应该至少仍然存在一个survivor区,类比其他的新生代收集器,这一点似乎并没有太大变化。另一个重要的变化是老年代的区域增多,因为部分survivor区或者eden区的对象可能会晋升到老年代。

Java GC G1 详解_老年代_02

三、G1并发标记周期

G1的并发阶段和CMS有些类似,它们都是为了降低一次停顿时间,而将可以和应用程序并发执行的部分单独提取出来执行。

并发标记周期针对老年代

并发标记周期可分为以下几步:

  • 初始标记:标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序在这个阶段必须停止执行。
  • 根区域扫描:由于初始标记必然会伴随一次新生代GC,所以在初始化标记后,eden被清空,并且存活对象被移到survivor区。在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。 这个过程是可以和应用程序并发执行的。但是根区域扫描不能和新生代GC同时发生(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),故如果恰巧此时需要新生代GC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代GC的时间就会延长。
  • 并发标记:和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发过程,并且这个过程可以被一次新生代GC打断。
  • 重新标记:和CMS一样,重新标记也是会使应用程序停顿,由于在并发标记过程中,应用程序依然运行,因此标记结果可能需要修正,所以在此阶段对上一次标记进行补充。在G1中,这个过程使用SATB(Snapshot-At-The-Begining)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
  • 独占清理:顾名思义,这个阶段会引起停顿。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。
  • 并发清理阶段:识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。
那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:
白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
灰:对象被标记了,但是它的field还没有被标记或标记完。
黑:对象被标记了,且它的所有field也被标记完了。

SATB 利用 write barrier 将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根 Stop The World 地重新扫描一遍即可避免漏标问题。 因此G1 Remark阶段 Stop The World 与 CMS了的remark有一个本质上的区别,那就是这个暂停只需要扫描有 write barrier 所追中对象为根的对象, 而 CMS 的remark 需要重新扫描整个根集合,因而CMS remark有可能会非常慢。

四、混合回收

在并发标记周期中,虽有部分对象被回收,但是回收的比例是非常低的。但是在并发标记周期后,G1已经明确知道哪些区域含有比较多的垃圾对象,在混合回收阶段,就可以专门针对这些区域进行回收。当然G1会优先回收垃圾比例较高的区域(回收这些区域的性价比高),这正是G1名字的由来(Garbage First Garbage Collector:译为垃圾优先的垃圾回收器),这里的垃圾优先(Garbage First)指的是回收时优先选取垃圾比例最高的区域。

这个阶段叫做混合回收,是因为在这个阶段,即会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收,同时处理了新生代和老年代。

混合回收会被执行多次,直到回收了足够多的内存空间,然后,它会触发一次新生代GC。新生代GC后,又可能会发生一次并发标记周期的处理,最后又会引起混合回收,因此整个过程可能是如下图:

Java GC G1 详解_垃圾回收_03

五、必要时的Full GC

和CMS类似,并发收集让应用程序和GC线程交替工作,因此在特别繁忙的情况下无可避免的会发生回收过程中内存不足的情况,当遇到这种情况,G1会转入一个Full GC 进行回收。

以下4种情况会触发这类的Full GC:

1、并发模式失效

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

GC日志如下的示例:

Java GC G1 详解_老年代_04


解决办法:发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整周期,让它运行得更快(如,增加后台处理的线程数)。

2、晋升失败

(to-space exhausted或者to-space overflow)

G1收集器完成了标记阶段,开始启动混合式垃圾回收,清理老年代的分区,不过,老年代空间在垃圾回收释放出足够内存之前就会被耗尽。(G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用),由此触发了Full GC。

下面日志中(可以在日志中看到(to-space exhausted)或者(to-space overflow)),反应的现象是混合式GC之后紧接着一次Full GC。

Java GC G1 详解_g1_05


这种失败通常意味着混合式收集需要更迅速的完成垃圾收集:每次新生代垃圾收集需要处理更多老年代的分区。

解决这种问题的方式是:

  1. 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
  2. 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
  3. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

3、疏散失败

(to-space exhausted或者to-space overflow)

进行新生代垃圾收集是,Survivor空间和老年代中没有足够的空间容纳所有的幸存对象。这种情形在GC日志中通常是:

Java GC G1 详解_gc_06


这条日志表明堆已经几乎完全用尽或者碎片化了。G1收集器会尝试修复这一失败,但可以预期,结果会更加恶化:G1收集器会转而使用Full GC。

解决这种问题的方式是:

  1. 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
  2. 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
  3. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

4、巨型对象分配失败

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

六、Humongous Objects and Humongous Allocations

For G1 GC, any object that is more than half a region size is considered a “Humongous object”. Such an object is allocated directly in the old generation into “Humongous regions”. These Humongous regions are a contiguous set of regions. StartsHumongous marks the start of the contiguous set and ContinuesHumongous marks the continuation of the set.

Before allocating any Humongous region, the marking threshold is checked, initiating a concurrent cycle, if necessary.

Dead Humongous objects are freed at the end of the marking cycle during the cleanup phase also during a full garbage collection cycle.

In-order to reduce copying overhead, the Humongous objects are not included in any evacuation pause. A full garbage collection cycle compacts Humongous objects in place.

Since each individual set of StartsHumongous and ContinuesHumongous regions contains just one humongous object, the space between the end of the humongous object and the end of the last region spanned by the object is unused. For objects that are just slightly larger than a multiple of the heap region size, this unused space can cause the heap to become fragmented.

If you see back-to-back concurrent cycles initiated due to Humongous allocations and if such allocations are fragmenting your old generation, please increase your -XX:G1HeapRegionSize such that previous Humongous objects are no longer Humongous and will follow the regular allocation path.

This solution can have side effects – increasing the region size reduces the number of regions available so you need to be careful and run extra set of tests to see whether or not you actually improved the throughput or latency of the application.

本小结摘自:
​​​https://www.oracle.com/technetwork/articles/java/g1gc-1984535.html​​​​https://plumbr.io/handbook/gc-tuning-in-practice/other-examples/humongous-allocations​​

七、常见调优参数

1、-XX:MaxGCPauseMillis=N,(默认200毫秒,与throughput收集器有所不同)

前面介绍过使用GC的最基本的参数:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。

  • Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
  • Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。

在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

2、其他调优参数

-XX:G1HeapRegionSize=n

设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。

-XX:ParallelGCThreads=n(调整G1垃圾收集的后台线程数)

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。

-XX:ConcGCThreads=n(调整G1垃圾收集的后台线程数)

设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

Java GC G1 详解_gc_07

-XX:InitiatingHeapOccupancyPercent=45(调整G1垃圾收集运行频率)

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

该值设置太高:会陷入Full GC泥潭之中,因为并发阶段没有足够的时间在剩下的堆空间被填满之前完成垃圾收集。

如果该值设置太小:应用程序又会以超过实际需要的节奏进行大量的后台处理。

避免使用以下参数:

避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

-XX:G1MixedGCLiveThresholdPercent=65

为混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%。这是一个实验性的标志。有关示例,请参见“如何解锁实验性虚拟机标志”。此设置取代了 -XX:G1OldCSetRegionLiveThresholdPercent 设置。Java HotSpot VM build 23 中没有此设置。

-XX:G1MixedGCCountTarget=8

设置标记周期完成后,对存活数据上限为 G1MixedGCLIveThresholdPercent 的旧区域执行混合垃圾回收的目标次数。默认值是 8 次混合垃圾回收。混合回收的目标是要控制在此目标次数以内。Java HotSpot VM build 23 中没有此设置。

-XX:G1OldCSetRegionThresholdPercent=10

设置混合垃圾回收期间要回收的最大旧区域数。默认值是 Java 堆的 10%。Java HotSpot VM build 23 中没有此设置。

-XX:G1ReservePercent=10

设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%。增加或减少百分比时,请确保对总的 Java 堆调整相同的量。Java HotSpot VM build 23 中没有此设置。

-XX:G1HeapWastePercent=10

设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,Java HotSpot VM 不会启动混合垃圾回收周期。默认值是 10%。Java HotSpot VM build 23 中没有此设置。

八、趋势

其实可以看到Java 垃圾回收器的趋势,就是在大内存堆的前提下尽 GC 可能的降低对应用程序的影响;从 CMS 的分阶段增量标记,到 G1 通过 SATB 算法改正 remark 阶段的 Stop The World 的影响,再到 Z/C4甚至在标记阶段无需 Stop The World,莫不如此。

​​即强调Throughput、Latency,降低对于Footprint的要求​​。

​​Java ZGC: A Scalable Low-Latency Garbage Collector​​

九、细节

1、G1 mixed GC时机?

mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.

在分配humongous object之前先检查是否超过 initiating heap occupancy percent, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。

为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。

一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。

2、XX:G1 HeapRegionSize 默认值?

默认把堆内存按照2048份均分,最后得到一个合理的大小。

3、直接内存配置

Q: 什么时候用直接内存?
A: 读写频繁的场合,出于性能考虑,可以考虑使用直接内存。

直接内存也是 Java 程序中非常重要的组成部分,特别是 NIO 被广泛使用之后,直接内存可以跳过 Java 堆,使 Java 程序可以直接访问原生堆空间。因此可以在一定程度上加快内存的访问速度。直接内存可以用 -XX:MaxDirectMemorySize 设置,默认值为最大堆空间,也就是 -Xmx。当直接内存达到最大值的时候,也会触发垃圾回收,如果垃圾回收不能有效释放空间,直接内存溢出依然会引起系统的 OOM。

一般而言直接内存在访问读写上直接内存有较大优势(速度较快),但是在内存空间申请的时候,直接内存毫无优势而言。

十、其它

1、JDK 12 对于G1改进

​​Abortable Mixed Collections for G1​​​​Promptly Return Unused Committed Memory from G1​​

推荐文档:​​https://github.com/jiankunking/books-recommendation/blob/master/GC/G1/Java 9 - Garbage Collection.pdf​​

本文整理自:​​《实战JAVA虚拟机 JVM故障诊断与性能优化》​​

个人微信公众号:

Java GC G1 详解_gc_08

作者:jiankunking ​


举报

相关推荐

0 条评论