JVM学习06节-G1收集器

G1垃圾收集器

1. G1概述

G1是一款面向服务端应用的垃圾收集器,Hotspot开发团队赋予它的使命是:未来可一个替换掉CMS收集器。G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,竟可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:

  • GC线程和应用线程并发执行,和CMS一样。
  • 空闲内存压缩时避免冗长的暂停时间。
  • 应用需要更多可预测的GC暂停时间。
  • 不希望牺牲太多的吞吐性能。

2. Region

之前介绍的分代收集器将整个堆分为年轻代、老年代和永久代,每个代的空间是确定的。而 G1保留了逻辑分代,采用分区算法。默认把堆内存分为1024个分区,将整个堆划分为一个个大小相等的小块( region,默认512K)。

后续垃圾收集的单位都是以region为单位的。G1在进行垃圾清理的时候就是将一个region的对象拷贝到另外一个region中

region逻辑上连续,物理内存地址不连续。同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。示意图如下:

H,它代表Humongous Object(H-obj),当分配的对象大于等于region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作

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

可以通过 -XX:G1HeapRegionSize 设置,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定。

3. Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,

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

Young GC过程示意图如下:

Young GC是并行的、Stop The World的,能够动态调整的(基于历史Young GC统计信息和用户自定义的停顿时间为目标)。

这时,我们需要考虑一个问题,如果仅仅GC新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念(Remembered Set),每个region中都有一个RSet,记录的是其他region中的对象引用本region对象的关系(谁引用了我的对象)。

3.1 RSet

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。

于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。

3.2 Card Table

一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。如下图

3.3 Young GC的阶段

  1. 根扫描(静态和本地对象被扫描)
  2. 更新RSet(处理dirty card队列更新RS)
  3. 处理RSet(检测从新生代指向老年代的对象)
  4. 对象拷贝(拷贝存活的对象到Survivor/old区域)
  5. 处理引用队列(软引用、弱引用、虚引用)

4. Mixed GC

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

  • 回收部分老年代

    参数:-XX:MaxGCPauseMillis,用来指定一个G1收集过程目标停顿时间,默认值200ms。

  • Mixed GC的触发也是由一些参数控制。比如XX:InitiatingHeapOccupancyPercent表示老年代占整个堆大小的百分比,默认值是45%,达到该阈值就会触发一次Mixed GC。

它的GC分为两个阶段:

  1. 全局并发标记(Global Concurrent Marking)
  2. 拷贝存活对象(Evacuation)

4.1 全局并发标记

全局并发标记阶段是基于SATB的,与CMS有些类似,但是也有不同的地方,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

  • 初始标记(initial mark)
    该阶段会STW(第一次暂停所有应用线程)

    扫描根集合,将所有通过根集合直达的对象压入扫描栈,等待后续的处理。在G1中初始标记阶段是借助Young GC的暂停进行的,不需要额外的暂停。虽然加长了Young GC的暂停时间,但是从总体上来说还是提高的GC的效率。。

  • 并发标记(Concurrent Marking)
    该阶段不需要STW。

    这个阶段不断的从扫描栈中取出对象进行扫描,将扫描到的对象的字段再压入扫描栈中,依次递归,直到扫描栈为空,也就是说trace了所有GCRoot直达的对象。同时这个阶段还会扫描SATB write barrier所记录下的引用。

  • 最终标记(Remark)
    这个阶段也是STW的(第二次暂停所以应用线程)

    这个阶段会处理在并发标记阶段write barrier记录下的引用,同时进行弱引用的处理。这个阶段与CMS的最大的区别是CMS在这个阶段会扫描整个根集合,Eden也会作为根集合的一部分被扫描,因此耗时可能会很长。

  • 清除垃圾(Cleanup)
    该阶段会STW(第三次暂停所以应用线程)

    清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,这个阶段并不会实际上去做垃圾的收集,只是去根据停顿模型来预测出CSet,等待evacuation阶段来回收。

4.2 拷贝存活对象

Evacuation阶段是全暂停的

该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。Evacuation阶段从第一阶段选出来的Region中筛选出任意多个Region作为垃圾收集的目标,这些要收集的Region叫CSet,通过RSet实现

筛选出CSet之后,G1将并行的将这些Region里的存活对象拷贝到其他Region中,这点类似于ParalledScavenge的拷贝过程,整个过程是完全暂停的。关于停顿时间的控制,就是通过选择CSet的数量来达到控制时间长短的目标。

5. STAB

5.1 三色标记法

垃圾回收的并发标记阶段,GC线程和应用线程是并发执行的,所以一个对象被标记之后,应用线程可能篡改对象的引用关系,从而造成对象的漏标、误标。其实误标没什么关系,顶多造成浮动垃圾,在下次GC还是可以回收的,但是漏标的后果是致命的,把本应该存活的对象给回收了,从而影响了程序的正确性。

为了解决在并发标记过程中,存活对象漏标的情况,GC HandBook把对象分成三种颜色(三色标记法):
1、黑色:根对象,或者该对象与它的子对象都被扫描。
2、灰色:对象本身被扫描,但还没扫描完该对象中的子对象。
3、白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象。

假设:在GC扫描C之前的颜色如下:

三色1

在并发标记阶段,应用线程改变了这种引用关系

A.c=C
B.c=null

得出如下结果:

三色2

在重新标记阶段扫描结果如下

三色3

这种情况下C会被当做垃圾进行回收。Snapshot的存活对象原来是A、B、C,现在变成A、B了,Snapshot的完整遭到破坏了,显然这个做法是不合理。

从上述图中可以看出,漏标的情况只会发生在白色对象中,且满足以下任意一个条件:
1、并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象。
2、并发标记时,应用线程删除所有灰色对象到该白色对象的引用。

对于第一种情况,利用后写屏障(post-write barrier),记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍。

对于第二种情况,利用先写屏障(pre-write barrier),将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一边。

5.2 SATB实现

本小节引自占小狼的博客:https://www.jianshu.com/p/9e70097807ba

SATB全称Snapshot-At-The-Beginning,是增量式标记清除垃圾收集器开发的一个算法,主要应用于垃圾收集的并发标记阶段,解决了CMS垃圾收集器重新标记阶段长时间STW的潜在风险。

Region包含了5个指针,分别是bottom、previous TAMS、next TAMS、top和end,如下图

其中previous TAMS、next TAMS是前后两次发生并发标记时的位置,全称top-at-mark-start

  1. 假设第n轮并发标记开始,将该Region当前的top指针赋值给next TAMS,在并发标记标记期间,分配的对象都在[next TAMS, top]之间,SATB能够确保这部分的对象都会被标记,默认都是存活的
  2. 当并发标记结束时,将next TAMS所在的地址赋值给previous TAMS,SATB给 [bottom, previous TAMS] 之间的对象创建一个快照Bitmap,所有垃圾对象能通过快照被识别出来
  3. 第n+1轮并发标记开始,过程和第n轮一样

SATB保证了在并发标记过程中新分配对象不会漏标

但如果在TAMS之前有一个白色对象W,被一个灰色对象G引用,在并发标记扫描到这个字段之前被赋值为null,切断了对象W和对象G之间的引用关系,对象W就有可能漏标,这就是白色对象被漏标的第二种情况?

G1中如何解决?

G1采用的是pre-write barrier解决这个问题。简单说就是在并发标记阶段,当引用关系发生变化的时候,通过pre-write barrier函数会把这种这种变化记录并保存在一个队列里,在JVM源码中这个队列叫satb_mark_queue。在remark阶段会扫描这个队列,通过这种方式,旧的引用所指向的对象就会被标记上,其子孙也会被递归标记上,这样就不会漏标记任何对象。

pre-write barrier最终执行逻辑:

通过G1SATBCardTableModRefBS::enqueue(oop pre_val)把原引用保存到satb mark queue中和RSet的实现类似,每个应用线程都自带一个satb mark queue.

在下一次的并发标记阶段,会依次处理satb mark queue中的对象,确保这部分对象在本轮GC是存活的。

CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue ,解决了CMS垃圾收集器重新标记阶段长时间STW的潜在风险=

6. 停顿预测模型

G1收集器突出表现出来的一点是通过一个停顿预测模型来根据用户配置的停顿时间来选择CSet的大小,从而达到用户期待的应用程序暂停时间。通过-XX:MaxGCPauseMillis参数来设置。这一点有点类似于ParallelScavenge收集器。

关于停顿时间的设置并不是越短越好。设置的时间越短意味着每次收集的CSet越小,导致垃圾逐步积累变多,最终不得不退化成Serial GC;停顿时间设置的过长,那么会导致每次都会产生长时间的停顿,影响了程序对外的响应时间。

Reference:

https://yunxitalk.iteye.com/blog/2421975

https://www.jianshu.com/p/548c67aa1bc0

https://juejin.im/entry/5af0832c51882567244deb44

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值