【JVM】—深入理解G1回收器——概念详解

时间:2024-10-17 08:55:44

深入理解G1回收器——概念详解

⭐⭐⭐⭐⭐⭐
Github主页????https://github.com/A-BigTree
笔记链接????https://github.com/A-BigTree/Code_Learning
⭐⭐⭐⭐⭐⭐

如果可以,麻烦各位看官顺手点个star~????

文章目录

  • 深入理解G1回收器——概念详解
    • 1 堆内存模型
    • 2 G1回收机制
    • 3 RSet&CSet
    • 4 SATB
    • 5 可预测停顿


G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

1 堆内存模型

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

在这里插入图片描述

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

在这里插入图片描述

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

2 G1回收机制

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
    在这里插入图片描述

在垃圾回收时,G1 的运行方式与 CMS 收集器类似。G1 并发执行全局标记来确定整个堆中对象的活跃度。标记阶段完成后,G1 知道哪些区域大部分是空的。它首先在这些区域进行收集,这通常会产生大量可用空间,这是也为什么G1回收机制被称为垃圾回收优先。G1将收集和压缩的操作集中在可能充满可回收对象(即垃圾)的区域上。同时G1 使用停顿预测模型来满足用户定义的暂停时间,并根据指定的暂停时间选择要收集的区域( Region)数量。

G1 将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩和释放内存。这个复制清除操作会并行执行来减少暂停时间并提高吞吐量。所以每次垃圾收集时,G1 都会在用户定义的暂停时间内持续工作来减少碎片空间

与CMS和ParallelOld的区别。

  • CMS(并发 标记清除 )垃圾收集器不进行整理或复制。
  • ParallelOld 垃圾收集是对整个堆进行整理,会造成相当长的SWT。

3 RSet&CSet

每个 Region 都有一个 Remembered Set(RSet),用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。

RSet与Card Table有些类似,是一种典型的空间换时间工具。在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会在对应的RSet中记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

在这里插入图片描述

RSet是怎么配合CSet进行快速垃圾分析的?

GC时GC Root的对象只可能在两个位置:

  • CSet里的Region
  • 非CSet里的Region

CSet是需要收集的region集合而非CSet里的region不需要收集,如果GC Root对象在CSet的Region里,依次遍历可达对象即可;如果GC Root在非CSet里,就需要依次遍历从非CSet到CSet里对象的引用即扫描所有非CSet region,非常耗时。

如果有了RSet,通过扫描CSet里所有Region的RSet就能知道不参与收集的其他Region对CSet中对象的引用,避免了全局扫描这些不参与收集的Region(有点绕????‍????)

RSet和CSet的引入不会影响JVM堆的利用率吗?

根据官网介绍,使用G1的JVM进程会更大一些,但RSets 的整体占用空间影响小于 5%,同时CSet中的所有活动数据在 GC 期间都会被清除(复制/移动),对 JVM 空间的影响不到 1%。

4 SATB

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉;
  • 灰:对象被标记了,但是它的field还没有被标记或标记完;
  • 黑:对象被标记了,且它的所有field也被标记完了;

由于并发阶段的存在,Mutator线程(应用/用户线程)和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是:

  1. 有至少一个黑色对象在自己被标记之后指向了这个白色对象
  2. 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用

对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier 将旧引用记录下来。

在这里插入图片描述

这种方式有个缺点,就是会产生浮动垃圾。 因为当用户线程取消引用的时候,有可能是真的取消引用,对应的对象是真的要回收掉的。这时候我们通过这种方式,就会把本该回收的对象又复活了,从而导致出现浮动垃圾。但相对于本该存活的对象被回收,这个代价还是可以接受的,毕竟在下次 GC 的时候就可以回收了。

CMS也有并发标记过程,它是怎么解决这个问题的呢?

CMS 回收器采用的是增量更新方案,即破坏第一个条件:「有至少一个黑色对象在自己被标记之后指向了这个白色对象」。

既然有黑色对象在自己标记后,又重新指向了白色对象。那么我就把这个黑色对象的引用记录下来,在后续「重新标记」阶段再以这个黑色对象为根,对其引用进行重新扫描。通过这种方式,被黑色对象引用的白色对象就会变成灰色,从而变为存活状态。

这种方式有个缺点,就是会重新扫描新增的这部分黑色对象,会浪费多一些时间。但是这段时间相对于并发标记整个链路的扫描,还是小巫见大巫,毕竟真正发生引用变化的黑色对象是比较少的。

5 可预测停顿

Pause Prediction Model 即停顿预测模型。它在G1中的作用是:

G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.

G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 停顿预测模型是以衰减标准偏差为理论基础实现的。

在这里插入图片描述

(有点复杂,感兴趣的小伙伴可以自己深入探索一下????‍????)