Garbage First(简称G1)收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。它是一款专门针对于拥有多核处理器和大内存的机器的收集器,在满足了GC响应时间的延迟可控的情况下,也会尽可能提高的程序的吞吐量。
G1收集器具备如下特性:
- 能与用户线程同时执行,完成并发收集
- GC过程会有整理内存的过程,不会产生内存碎片,并且整理空闲内存速度更快
- GC发生时,停顿时间可控,可以让程序更大程度上追求低延迟
- 追求低延迟的同时,尽可能会保证高吞吐量
- 对于堆的未使用内存可以返还给操作系统
核心概念
为了实现高吞吐、没有内存碎片及收集时间可控等功能,G1引入了一些新的核心概念,如堆内存分区Region
,原始快照STAB
(Snapshot-At-The-Beginning
)、记忆集Remembered Set
。G1收集器将Region作为单次回收的最小单元,每次垃圾回收时根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的Region。
堆内存划分
区别于固定大小以及固定数量的分代收集器,G1是把堆划分为多个大小相等的独立区域(Region
)。每个Region根据需要可以扮演Eden
/Survivor
/Old
/Humongous
每个Region的大小可以通过参数
-XX:G1HeapRegionSize
设定,取值范围从1M到32M,且应为2的N次幂。如果不设定,那么G1会根据Heap大小自动决定
Region中特殊的Humongous
区域,专门用来存储大对象(G1设定大小超过了一个Region容量一半的对象即为大对象)
对于超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待
SATB
原始快照(SATB:Snapshot At The Beginning),是G1收集器解决并发标记阶段对象消失问题的算法。并发标记过程会以最初的对象图关系进行访问,就算并发标记过程中某个对象的引用信息发生了改变,G1会通过写前屏障记录原有的对象引用关系,依旧会按照最初的对象图快照进行标记。
G1为每一个Region设计了两个名为
TAMS
(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围
Rset
Rset全称为Remembered Set,是辅助GC过程的一种结构,和Card Table一样同为记忆集的一种实现,是典型以空间换时间的工具。
还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。
逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。
RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index
Rset & Region
下图表示了RSet、Card和Region的关系(出处):
上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。
在虚拟机运行期间,RSet中的引用关系靠
post-write barrier
和Concurrent refinement threads
来维护
Rset作用
- Young GC时,只需要选定年轻代Region的RSet作为根集,这些RSet记录了
old->young
的跨代引用,避免了扫描整个年老代。 - Mixed GC时,年老代中记录了
old->old
的RSet,young->old
的引用由扫描全部年轻代Region得到,从从而避免了扫描全部年老代Region
GC类型
G1中主要存在YoungGC
、MixedGC
以及FullGC
三种GC类型
Young GC
G1对于整个堆空间所有的Region区不会在一开始就全部分配完,无论是新生代、幸存区以及年老代在最开始都是会有初始数量的,在程序运行过程中会根据需求不断增加每个分代区域的Region数量
G1 YoungGC并非说Eden
区放满了就会立马被触发。 当Eden
区被用完时,G1首先会大概计算一下回收当前的年轻代空间需要花费多少时间,如果回收时间远远小于参数-XX:MaxGCPauseMills
设定的值,那么不会触发YoungGC
,而是会继续为Eden
区增加新的Region区用于存放新分配的对象实例。直至某次Eden
区空间再次被放满并经过计算后,此次回收的耗时接近-XX:MaxGCPauseMills
参数设定的值,那么才会触发YoungGC
- Young GC只回收
Eden
区和Survivor
区- G1收集器中的新生代收集,依旧保留了分代收集器的特性,当YoungGC被触发时,拷贝存活对象到新Survivor Region,年龄大于max threshold则进入老年代,并处理引用
Mixed GC
MixedGC(混合型GC),当整个堆中年老代的区域占有率达到参数-XX:InitiatingHeapOccupancyPercent
设定的值后触发MixedGC,发生该类型GC后,会回收所有新生代Region区、部分年老代Region区(会根据期望的GC停顿时间选择合适的年老代Region区优先回收)以及大对象Humongous区
正常情况下,G1垃圾收集时会先发生
MixedGC
,主要采用复制算法,在GC时先将要回收的Region中存活的对象拷贝至别的Region内,拷贝过程中,如果发现没有足够多的空闲Region承载拷贝对象,此时就会触发一次Full GC
Full GC
当整个堆空间中的空闲Region不足以支撑拷贝对象或由于元数据空间满了等原因,会触发FullGC
。G1首先会停止系统所有用户线程,然后采用单线程进行标记、清理和压缩整理内存,以便于清理出足够多的空闲Region来供下一次MixedGC
使用
其实G1收集器中并没有FullGC,G1中的
FullGC
是采用Serial old FullGC
。因为G1在设计时的初衷就是要避免发生FullGC
,如果上述两种GC发生后还是无法使得程序恢复正常执行,最终就会触发SerialOld
收集器的FullGC
最新版本jdk的G1 FullGC 是并行进行的了,在通用场景中的表现还优于 Parallel GC 的 Full GC 实现
垃圾回收过程
G1收集器一般在发生GC时执行过程大致会分为四个步骤(主要指MixedGC):
- 初始标记(InitialMark):先触发STW,然后使用单条GC线程快速标记
GC Roots
直连的对象,并修改TAMS
指针的值 (借用Young GC时同步完成) - 并发标记(ConcurrentMarking):从
GC Roots
开始对堆中对象进行可达性分析,标记要回收的对象。扫描完成后重新处理STAB
记录下的在并发时有引用变动的对象 - 最终标记(Remark):纠正并发标记阶段因用户操作导致的错标、误标、漏标对象。
- 筛选回收(Cleanup):先对各个Region区的回收价值和成本进行排序,找出「回收价值最大」的Region优先回收。
G1收集器除了并发标记外,其余截断都要完全暂停用户线程
G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集的过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息
G1 JVM参数
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:G1HeapWastePercent | 在global concurrent marking 结束之后,我们可以知道old gen regions 中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC |
-XX: G1MixedGCLiveThresholdPercent | old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet |
-XX:G1MixedGCCountTarget | 一次global concurrent marking 之后,最多执行Mixed GC 的次数 |
-XX:G1OldCSetRegionThresholdPercent | 一次Mixed GC 中能被选入CSet 的最多old generation region 数量 |
-XX:+UseStringDeduplication | JVM在GC时会做重复字符串消除 |
G1 GC日志分析
jdk17 G1 GC日志
Pause Young (Normal) (G1 Evacuation Pause) GC原因,Young GC
Heap before GC invocations=12555 (full 0): GC前对象引用计数
garbage-first heap total 2072576K, used 1874869K [0x0000000741800000, 0x0000000800000000) 使用了G1垃圾收集器,堆总大小2072576K,使用了1874869K
region size 2048K, 759 young (1554432K), 3 survivors (6144K) Region大小为2M,青年代占用了759个(共1554432K),幸存区占用了3个(共6144K)
Metaspace used 85721K, committed 86592K, reserved 344064K 元数据取大小(没有设置MetaspaceSize=MaxMetaspaceSize,则会有committed和reserved)
class space used 8771K, committed 9280K, reserved 212992K
Using 4 workers of 4 for evacuation
Desired survivor size 265289728 bytes, new threshold 15 (max threshold 15)
Pre Evacuate Collection Set: 0.3ms 预处理共耗时0.3ms,包括选择Region,扫描Root,更新添加Region到Collection Set等
Merge Heap Roots: 0.2ms 扫描合并Rset
Evacuate Collection Set: 2.6ms 拷贝存活对象到新region,年龄大于max threshold则进入老年代,并处理引用
Post Evacuate Collection Set: 1.2ms 垃圾处理后操作,包括更新code root 引用,清除card table等
Other: 0.1ms 其他事项共耗时0.1ms,其他事项包括选择CSet,处理已用对象,引用入ReferenceQueues,释放CSet中的Region到free list
Age table with threshold 15 (max threshold 15)
- age 1: 2223472 bytes, 2223472 total
- age 2: 719736 bytes, 2943208 total
- age 3: 60552 bytes, 3003760 total
- age 4: 278744 bytes, 3282504 total
- age 5: 1680 bytes, 3284184 total
- age 6: 5328 bytes, 3289512 total
- age 7: 1120 bytes, 3290632 total
- age 8: 2752 bytes, 3293384 total
- age 9: 64880 bytes, 3358264 total
- age 10: 138360 bytes, 3496624 total
- age 11: 186472 bytes, 3683096 total
- age 12: 12144 bytes, 3695240 total
- age 13: 336 bytes, 3695576 total
- age 14: 6368 bytes, 3701944 total
- age 15: 70752 bytes, 3772696 total
Eden regions: 756->0(757) 垃圾回收前后Eden区数量(Eden区总数为757)
Used: 0K, Waste: 0K
Survivor regions: 3->2(253) 垃圾回收前后Survivor区数量(Survivor区总数为253)
Used: 4072K, Waste: 23K
Old regions: 73->73
Used: 147878K, Waste: 1625K
Archive regions: 2->2
Used: 2016K, Waste: 2080K
Humongous regions: 85->7
Used: 14336K, Waste: 0K
Metaspace: 85721K(86592K)->85721K(86592K) NonClass: 76949K(77312K)->76949K(77312K) Class: 8771K(9280K)->8771K(9280K)
Heap after GC invocations=12556 (full 0):
garbage-first heap total 2072576K, used 168302K [0x0000000741800000, 0x0000000800000000)
region size 2048K, 2 young (4096K), 2 survivors (4096K)
Metaspace used 85721K, committed 86592K, reserved 344064K
class space used 8771K, committed 9280K, reserved 212992K
Pause Young (Normal) (G1 Evacuation Pause) 1832M->164M(2024M) 4.796ms 此次Young GC从1832M回收至164M,耗时4.796ms
User=0.02s Sys=0.00s Real=0.00s
Safepoint "G1CollectForAllocation", Time since last: 85999229627 ns, Reaching safepoint: 49914 ns, At safepoint: 4898699 ns, Total: 4948613 ns 安全点为G1CollectForAllocation
参考资料: