目录
一、基本背景概述
内存划分简介
二、重点基础概念介绍
(一)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.
从官网的描述中,它是专门针对以下应用场景设计的:
之前的垃圾收集器(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 个个 region 的,region 的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。
G1 将 heap 内存区,划分为一个个大小相等(通过参数-XX:G1HeapRegionSize=x,x=1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,所以内存还是通过分代收集的,但是 region 与 region 之间不要求连续。具体图示在上面已经展示过。
在每个Region(分区)内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
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背景:由于 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是维持并发GC的正确性的一个手段,G1GC的并发理论基础就是SATB,SATB是由Taiichi Yuasa为增量式标记清除垃圾收集器设计的一个标记算法。
SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous位图和next位图。previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉。
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 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%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程,年轻代回收只回收Eden区和Survivor区。
YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。如图,回收完E和S区,剩余存活的对象会复制到新的S区,S区达到一定的阈值可以晋升为O区。
细致过程:
- 如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
- 如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
基本步骤如下展示:
初始标记阶段
标记从根节点直接可达的对象。这个阶段是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回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。这个阶段并不会实际上去做垃圾的收集
并发清理阶段
识别并清理完全空闲的区域。
当越来越多的对象晋升到老年代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会停止应用程序的执行(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)等优势,其主要适用场景如下
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长于0.5至1秒)
介绍一些关于 G1 垃圾回收器优化的一般性建议:
需要注意的是,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=n | eden/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.mdhttps://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课程