JVM - G1

点击上方“xy的技术圈”,选择“设为星标

JVM - G1

认真写文章,用心做分享。

微信公众号:xy的技术圈

个人网站:yasinshaw.com

JVM - G1

正文

什么是G1

Garbage-First收集器是一个并行、并发和增量压缩低停顿的垃圾收集器。

G1的Java堆布局和HotSpot VM中其他垃圾收集器有着极大的不同,它将Java的堆分成相同的块(称为区域,Region)。

G1也是分代的,但整体上没有划分成新生代和老年代。相反,每代是一组(可能不连续)的Region,这使得它可以灵活地调整新生代。

为什么需要了解G1?

因为G1是JDK 11默认的垃圾收集器,而JDK 11是目前Java最近的LTS。所以还是有必要了解一下G1。

延迟与吞吐量指标

在系统设计的过程必须考虑系统的响应能力(低延迟)与吞吐量两个指标。

  • 低延迟:指系统从接受到请求到返回相应数据的速度,简单理解应用以多快的速度返回请求的数据,例如以多快的速度(ms)打开一个网页。对于面对响应能力的应用来说,长时间的停顿是不可接受的。

  • 吞吐量:指系统在某个指定的时间范围内能够处理的最大工作量,比如每秒钟完成了多少次数据库删除或者查询。对于面对吞吐量的应用来讲,较长的停顿时间是能够接受的,高吞吐量应用关注的是系统处理能力
    这两个指标往往是成对出现的,往往是为了满足一个而不得不牺牲另外一个。

G1特点

  • 低延迟优先,即主要侧重于响应能力;

  • 与CMS相同的地方在于,它们都属于并发收集器,在大部分的收集阶段都不需要挂起应用程序;

  • 更精确的预测GC停顿时间,可以根据-XX:MaxGCPauseMillis参数指定停顿时间;

  • 收缩空闲空间不会造成由长GC引起的应用停顿时间;

  • G1没有CMS的碎片化问题(或者说不那么严重)。

G1内存区域是怎么划分的

G1 将整个堆划分为一个个大小相等的小块(每一块称为一个Region),每一块的内存是连续的。Region的大小可以通过参数-XX:G1HeapRegionSize指定,若未指定则设置默认值:最小Region为1M、最大为32M、默认Region个数为2048个。

JVM - G1

和分代算法一样,G1中每个块也会充当Eden、Survivor、Old 三种角色,但是它们不是固定的,这使得内存使用更加地灵活。

H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。当分配的对象大于或等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。如果发现堆内存容不下H对象的时候,会触发一次GC操作。

执行垃圾收集时,和CMS一样,G1收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1也就知道哪些区块基本上是垃圾,存活对象极少,G1会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间,这也是为什么G1被取名为Garbage-First的原因。

RSet和Card Table

在进行Young GC的时候,Young区的对象可能还存在Old区的引用,这就是跨代引用的问题。

为了解决Young GC的时候,扫描整个老年代,G1引入了Card Table和Remember Set的概念,基本思想就是用空间换时间

这两个数据结构是专门用来处理Old区到Young区的引用,相关Old区的对象将被存储在这两个数据结构中,用这两个数据结构空间,避免了Yong GC扫描整个老年代,这就是用空间换时间

这两个数据结构是专门用来处理Old区到Young区的引用。Young区到Old区的引用则不需要单独处理,因为Young区中的对象本身变化比较大,没必要浪费空间去记录下来。

  • RSet:全称Remembered Sets, 用来记录外部指向本Region的所有引用,每个Region维护一个RSet。

  • Card: JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。

下图展示的是RSet与Card的关系。每个Region被分成了多个Card,其中绿色部分的Card表示该Card中有对象引用了其他Card中的对象,这种引用关系用蓝色实线表示。

RSet其实是一个HashTable,Key是Region的起始地址,Value是Card Table (卡表,字节数组),字节数组下标表示Card的空间地址,当该地址空间被引用的时候会被标记为dirty_card(脏卡)。而在Yong GC的过程中,垃圾收集器只会在脏卡中扫描老年代-新生代引用。

JVM - G1

对象分配策略

对象的分配策略,分为3个阶段:

  1. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区

  2. Eden区中分配

  3. Humongous区分配

TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。

对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

三种GC

Yong GC

Eden空间耗尽时会被触发,回收的是所有年轻代的Region。

年轻代的垃圾收集和其它收集器相差不大,都是采用复制算法,把对象复制到Survivor区,或晋升至Old区(达到晋升年龄或者Survivor空间不够)。不同的是,G1进行一次Yong GC后,会基于历史Yong GC统计信息和用户定义的期望停顿时间,调整Eden区和Survivor区的大小

E区的对象会移动到S区,当S区空间不够的时候,E区的对象会直接晋升到O区,同时S区的数据移动到新的S区,如果S区的部分对象到达一定年龄,会晋升到O区。最终Eden空间的数据为空,GC停止工作,应用线程继续执行

上面说到在Yong GC过程中会出现 跨代引用问题,不过G1通过RSet和Card两种数据结构,以空间换去时间的处理方案解决此问题,具体过程可参考上一小节。

Young GC的阶段:

  1. 根扫描:静态和本地对象被扫描

  2. 更新RS:处理dirty card队列更新RS

  3. 处理RS:检测从年轻代指向年老代的对象

  4. 阶段4:对象拷贝:拷贝存活的对象到survivor/old区域

  5. 处理引用队列:软引用,弱引用,虚引用处理

Mix GC

当old区Heap的对象占总Heap的比例超过InitiatingHeapOccupancyPercent之后,就会开始并发标记, 完成了并发标记后,G1会从Young GC切换到Mixed GC, 在Mixed GC中,会回收所有的年轻代的Region和部分老年代的Region。

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

Mix GC步骤分2步:
1. 全局并发标记(global concurrent marking)

阶段 作用
初始标记(initial mark,STW) 它标记了从GC Root开始直接可达的对象。初始标记阶段借用Young GC的暂停,因而没有额外的、单独的暂停阶段。
根区扫描 G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking) 这个阶段从GC Root开始对Heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。过程中还会扫描SATB write barrier所记录下的引用。
最终标记(Remark,STW) 标记那些在并发标记阶段发生变化的对象,将被回收
清除垃圾(Cleanup,部分STW) 这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。清除空Region。

2. 拷贝存活对象(evacuation)
拷贝(Evacuation)阶段是STW(Stop The World)的。它负责把一部分Region里的活对象拷贝到空Region里去(并行拷贝),然后回收原本的Region的空间。

Evacuation阶段可以*选择任意多个Region来独立收集,构成收集集合(collection set,简称CSet),CSet集合中Region的选定依赖于上文中提到的停顿预测模型,该阶段并不evacuate所有有活对象的Region,只选择收益高的少量Region来Evacuate,这种暂停的开销就可以(在一定范围内)可控。

混合垃圾回收周期会持续进行,直到几乎所有的被标记出来的分区(垃圾占比大的分区)都得到回收,然后恢复到常规的年轻代垃圾收集,最终再次启动并发标记。

Full GC

当Mixed GC的速度赶不上应用程序申请内存的速度的时候,Mixed G1就会降级到Full GC,使用的是Serial GC。Full GC会导致长时间的STW,应该要尽量避免

还记得在之前的文章中介绍过的CMF吗?在CMS中叫做Concurrent Mode Failure, 在G1中称为Allocation Failure

一张完整收集过程图:

JVM - G1

哪些情况会导致Full GC?如何避免?

  1. Allocation Failure:G1并发标记期间,如果在标记结束前,老年代被填满,G1会放弃标记。这个时候说明:

  • 堆需要增加了

  • 或者需要调整并发周期,如增加并发标记的线程数量,让并发标记尽快结束

  • 或者就是更早地进行并发标记,默认是整堆内存的 45% 被占用就开始进行并发标记。

  1. 晋升失败:并发周期结束后,是混合垃圾回收周期,伴随着年轻代垃圾收集,进行清理老年代空间,如果这个时候清理的速度小于消耗的速度,导致老年代不够用,那么会发生晋升失败。

说明混合垃圾回收需要更迅速完成垃圾收集,也就是说在混合回收阶段,每次年轻代的收集应该处理更多的老年代已标记区块。

  1. 疏散失败:年轻代垃圾收集的时候,如果Survivor和Old区没有足够的空间容纳所有的存活对象。这种情况肯定是非常致命的,因为基本上已经没有多少空间可以用了,这个时候会触发Full GC也是很合理的。

最简单的就是增加堆大小

  1. 大对象分配失败:

我们应该尽可能地不创建大对象,尤其是大于一个Region大小的那种对象。

常见的参数设置

JVM - G1

END

JVM - G1

JVM - G1

推荐阅读

JVM - G1

↓↓↓ 点击"阅读原文" 去【评论】吧