Java Hotspot G1 GC的理解总结

目录

一、基本背景概述

内存划分简介

二、重点基础概念介绍

(一)Region(分区)

(二)Card(卡片)

(三)CSet(待回收Region集合)

(四)RSet(引用索引集合)

(五)SATB(snapshot-at-the-beginning)

(六) Marking bitmap(位图)和TAMS

三、G1收集器垃圾回收过程

G1回收过程一:年轻代 GC

G1回收过程二:并发标记过程

G1回收过程三:混合回收过程

G1 回收可选的过程四:Full GC

四、适用场景分析

五、应用建议

六、常用参数展示

参考文献、书籍及链接


一、基本背景概述

Garbage-First (G1) 收集器是一种服务器式垃圾收集器,针对具有大内存的多处理器机器。它尽可能地满足目标暂停时间,同时兼顾高吞吐量。全称Garbage-First Garbage Collector,通过参数来启用-XX:+UseG1GC,在JDK 7u4版本发行时被正式推出,在JDK 9中被提议设置为默认垃圾收集器(JEP 248)。官网描述如下:

The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:

> * Can operate concurrently with applications threads like the CMS collector.

> * Compact free space without lengthy GC induced pause times.

> * Need more predictable GC pause durations.

> * Do not want to sacrifice a lot of throughput performance.

> * Do not require a much larger Java heap.

从官网的描述中,它是专门针对以下应用场景设计的: 

  • 像CMS收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。 
  • 需要GC停顿时间更好预测。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。

内存划分简介

之前的垃圾收集器(serial, parallel, CMS)都将堆结构划分为三个部分:新生代、老年代和固定大小的永久代。这些收集器都只针对其中一个部分进行垃圾收集。

鉴于CMS 的一些不足,比如: STW时长与内存大小显著正相关(超大堆停顿时间长)、STW时长不可控、老年代内存碎片化,G1 就横空出世了。设计初衷是为了尽量缩短处理超大堆时产生的停顿。可以设置-XX:MaxGCPauseMillis,控制GC的停顿时间。G1在回收的时候将对象从一个小堆区复制到另一个小堆区,这意味着G1在回收垃圾的时候同时完成了堆的部分内存压缩,相对于CMS的优势而言就是内存碎片的产生率大大降低。

G1 对于 heap 区的内存划思路很新颖,将 heap 内存区,划分为一个个大小相等(1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,所以内存还是通过分代收集的,但是 region 与 region 之间不要求连续。

在上图中,标明H代表Humongous,是Region存储的巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:

  1. H-obj直接分配到了old gen,防止了反复拷贝移动。
  2. H-obj在global concurrent marking阶段的cleanup 和 full gc阶段回收。
  3. 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full gc。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间以及回收所需时间的经验值),维护一个优先列表,根据允许收集的时间,优先回收价值最大的Region。这种方式优先清除高价值的垃圾,所以称为:垃圾优先。

二、重点基础概念介绍

(一)Region(分区)

G1 将 heap 内存区,划分为一个个大小相等(通过参数-XX:G1HeapRegionSize=x,x=1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,所以内存还是通过分代收集的,但是 region 与 region 之间不要求连续。具体图示在上面已经展示过。

(二)Card(卡片)

每个Region(分区)内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

(三)CSet(待回收Region集合)

CSet记录的是GC等待收集的Region的集合,CSet里的Region可以是任意代的。总体上CSet消耗的内存小于 1%,G1垃圾回收器的软实时的特性就是通过CSet的选择来实现的

 对应于算法的两种模式fully-young generational mode和partially-young mode:

fully-young generational mode:该模式下CSet将只包含young的Region,调整young的Region的数量来匹配软实时的目标;

partially-young mode:该模式会选择所有的young region,并且选择一部分的old region。old region的选择将依据在Marking cycle phase中对存活对象的计数。G1选择存活对象最少的Region进行回收。

(四)RSet(引用索引集合)

RSet背景:由于 region 与 region 之间并不要求连续,而使用 G1 的场景通常是大内存,比如 64G 甚至更大,为了提高扫描根对象和标记的效率

RSet所占用的JVM内存小于总大小的5%。数据结构为hashtable,key为外部分区(Region)的起始地址,value为引用对象所在的卡片(Card)的索引。每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

RSet对young/mixed GC的性能非常有帮助(不必扫描所有老年代Region,只需要扫描RSet),随之而来的就是RSet的维护成本:写屏障(write barrier)与并发优化线程,写栅栏交由应用线程(mutator)执行,其原理为在所有指针修改操作之后插入写栅栏代码(可以理解为指令级AOP)

(五)SATB(snapshot-at-the-beginning)

SATB是维持并发GC的正确性的一个手段,G1GC的并发理论基础就是SATB,SATB是由Taiichi Yuasa为增量式标记清除垃圾收集器设计的一个标记算法

SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous位图和next位图。previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉。

(六) Marking bitmap(位图)和TAMS

Marking bitmap是一种数据结构,其中的每一个bit代表的是一个可用于分配给对象的起始地址。

其中addrN代表的是一个对象的起始地址。绿色的块代表的是在该起始地址处的对象是存活对象,而其余白色的块则代表了垃圾对象。
G1使用了两个bitmap,一个叫做previous bitmap,另外一个叫做next bitmap。previous bitmap记录的是上一次的标记阶段完成之后的构造的bitmap;next bitmap则是当前正在标记阶段正在构造的bitmap。在当前标记阶段结束之后,当前标记的next bitmap就变成了下一次标记阶段的previous bitmap。
TAMS(top at mark start)变量,是一对用于区分在标记阶段新分配对象的变量,分别被称为previous TAMS和next TAMS。在previous TAMS和next TAMS之间的对象则是本次标记阶段时候新分配的对象。

白色region代表的是空闲空间,绿色region代表是存活对象,橙色region代表的在此次标记阶段新分配的对象。注意的是,在橙色区域的对象,并不能确保它们都事实上是存活的。

三、G1收集器垃圾回收过程

G1 GC的垃圾回收过程主要包括如下三个环节:

  1. 年轻代GC(Young GC)
  2. 老年代并发标记过程(Concurrent Marking)
  3. 混合回收(Mixed GC)

如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。

具体说明:

应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。

当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

举例:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

G1回收过程一:年轻代 GC

JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程,年轻代回收只回收Eden区和Survivor区。

YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。如图,回收完E和S区,剩余存活的对象会复制到新的S区,S区达到一定的阈值可以晋升为O区。

 细致过程:

  • 第一阶段:扫描根,GC Roots根引用连同RSet记录的外部引用作为扫描存活对象的入口。
  • 第二阶段:更新RSetG1回收过程二:并发标记过程
  • 第三阶段:处理RSet,识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
  • 第四阶段:复制对象。遍历对象树,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象
  • 如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
  • 如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
  • 第五阶段:处理引用,处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

G1回收过程二:并发标记过程

基本步骤如下展示:

初始标记阶段

标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。正是由于该阶段时STW的,所以我们只扫描根节点可达的对象,以节省时间。

根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在Young GC之前完成,因为Young GC会使用复制算法对Survivor区进行GC。

并发标记(Concurrent Marking)

在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

再次标记(Remark)

由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的原始快照算法:Snapshot-At-The-Beginning(SATB)。

独占清理(cleanup,STW)

计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。这个阶段并不会实际上去做垃圾的收集

并发清理阶段

识别并清理完全空闲的区域。

G1回收过程三:混合回收过程

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

-XX:G1MixedGCCountTarget:默认是8,意味着混合回收GC数目目标为8个,每次混合回收暂停的最小老年代Region数目的计算公式:每一次混合回收暂停的最小老年代区间数目=混合回收循环确认的候选老年代区间总数 / G1MixedGCCountTarget。

时机触发:XX:InitiatingHeapOccupancyPercent=45,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc

MixedGC的回收过程如下:

扩展注意事项:

并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收。

混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考年轻代回收过程。

由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收。XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

G1 回收可选的过程四:Full GC

G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

要避免Full GC的发生,一旦发生Full GC,需要对JVM参数进行调整。不论是年轻代还是老年代,G1都采用复制算法进行收集,因此需要空闲分区才能正常进行。若G1在执行的过程中,没有足够的空闲分区,则会导致full GC的发生。可能的情况为:

1)to space exhausted:Young GC过程中Survivor 和 Old 区无法找到新的空闲分区,导致疏散失败

2)promotion failure:Mixed GC实在无法跟上程序分配内存的速度,导致对象晋升失败

3)concurrent mode failure:Mixed GC如果在标记结束前,老年代被填满,G1 会放弃标记,导致并发模式失败

4)在分配巨型对象时无法找到合适的空闲分区,导致大对象分配失败

5)程序显式调用System.gc()。这里有个例外,若加上参数-XX:+ExplicitGCInvokesConcurrent,则G1会强行启动一次全局并发标记。很多NIO框架都设置此参数(为了回收堆外内存),从而避免引发full GC而导致性能下降

四、适用场景分析

基于其基本的优势,如并行与并发兼备、分代收集、空间整合、可预测的停顿时间模型(即:软实时soft real-time)等优势,其主要适用场景如下

  • 面向服务端应用,针对具有大内存、多处理器的机器(在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒)。(在普通大小的堆里表现并不惊喜)
  • 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
  • 用来替换掉JDK1.5中的CMS收集器,在下面的情况时,使用G1可能比CMS好:
  • 超过50%的Java堆被活动数据占用;
  • 对象分配频率或年代提升频率变化很大;
  • GC停顿时间过长(长于0.5至1秒)
  • HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器均使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

五、应用建议

介绍一些关于 G1 垃圾回收器优化的一般性建议:

  1. 调整区域大小:在使用 G1 垃圾回收器时,可以根据应用内存大小和分配情况调整各个区域的大小。例如,可以增加年轻代的大小,以减少对象晋升老年代的频率,同时调整老年代的大小,以充分利用多线程并发处理垃圾回收。
  2. 调整并发线程数:在进行 G1 垃圾回收时,可以根据机器配置和应用负载情况调整并发垃圾回收线程数。例如,可以增加并发垃圾回收线程数以加速垃圾回收速度和提高吞吐量。
  3. 设置目标停顿时间:在进行 G1 垃圾回收时,可以通过设置目标停顿时间来平衡垃圾回收速度和响应时间。例如,可以设置较短的目标停顿时间来保证应用的响应速度,但这可能会导致垃圾回收的效率降低。
  4. 分析 GC 日志:在进行 G1 垃圾回收优化时,可以通过分析 GC 日志来了解垃圾回收的实际情况,从而进行针对性的调优。例如,可以根据 GC 日志来确定哪些对象占用了大量内存,从而进行内存泄漏的排查和解决。

需要注意的是,G1 垃圾回收器的优化需要根据具体的应用场景和需求进行,不能一概而论。在进行 G1 垃圾回收器的优化时,需要结合实际情况进行参数调整和性能监控,以达到最优的性能和稳定性表现。

六、常用参数展示

选项/默认值说明
-XX:+UseG1GC使用 G1 (Garbage First) 垃圾收集器
-XX:MaxGCPauseMillis=n

设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal),这是一个大概值,JVM 会尽可能的满足此值。(默认200ms)。

代替使用平均响应时间(ART)做为指标,考虑设置值将会符合这个时间的90%或者更高比例。这意味着90%的用户发出一个请求将不会经历高于这个目标的时间。暂停时间只是一个目标,不保证总是能够达到。

-XX:InitiatingHeapOccupancyPercent=n设置触发标记周期的 Java 堆占用率阈值。启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认占用率是整个 Java 堆的 45%,默认值为 45.
-XX:NewRatio=n老年代与年轻代(old/new generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=neden/survivor 空间大小的比例(Ratio). 默认值为 8.
-XX:GCTimeRatio吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:MaxTenuringThreshold=n提升年老代的最大临界值(tenuring threshold). 默认值为 15.
-XX:ParallelGCThreads=n设置STW工作线程数的值,与使用的CPU的数量有关,最大值为8。如果CPU数量超过8个,则最多可以设置总CPU数量的 5/8。
-XX:ConcGCThreads=n设置并行标记线程数
-XX:G1ReservePercent=n设置预留空间的空闲百分比,以降低目标空间的溢出风险,默认为10%
-XX:G1HeapRegionSize=n使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.
-XX:G1NewSizePercent=n设置年轻代最小使用的空间比率,默认为Java堆内存的50%
-XX:G1MaxNewSizePercent=n设置年轻代最大使用的空间比率,默认为Java堆内存的60%
XX:G1HeapWastePercent堆废物百分比(默认5%)
-XX:G1MixedGCCountTarget参数混合周期的最大总次数(默认8)
-XX:G1PrintRegionLivenessInfo默认值false, 在情理阶段的并发标记环节,输出堆中的所有 regions 的活跃度信息
-XX:G1PrintHeapRegions默认值false, G1 将输出那些 regions 被分配和回收的信息
-XX:+PrintSafepointStatistics输出具体的停顿原因
-XX:+PrintGCApplicationStoppedTime停顿时间输出到GC日志中
-XX:-UseBiasedLocking取消偏向锁
-XX:+UseGCLogFileRotation开启滚动日志输出,避免内存被浪费
-XX:+PerfDisableSharedMem关闭 jstat 性能统计输出特性,使用 jmx 代替
-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)

参考文献、书籍及链接

1.Java Hotspot G1 GC的一些关键技术 - 美团技术团队

2.G1收集器与CMS收集器的对比与实战 - Chris Blog - Java博文专集

3.一文看懂JVM内存布局及GC原理_技术管理_杨俊明_InfoQ精选文章

4.《深入理解Java虚拟机》

5.Getting Started with the G1 Garbage Collector

6.https://github.com/youthlql/JavaYouth/blob/main/docs/Java/JVM/JVM%E7%B3%BB%E5%88%97-%E7%AC%AC12%E7%AB%A0-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8.mdicon-default.png?t=N7T8https://github.com/youthlql/JavaYouth/blob/main/docs/Java/JVM/JVM%E7%B3%BB%E5%88%97-%E7%AC%AC12%E7%AB%A0-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8.md

7.尚硅谷深入理解JVM课程

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张彦峰ZYF

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

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

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

打赏作者

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

抵扣说明:

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

余额充值