JVM 性能分析—— 一文带你读懂 G1 垃圾收集器收集流程

Garbage First 收集器(G1)中的概念

介绍

G1 设计目的

JVM 性能分析—— 一文带你读懂 CMS 垃圾收集器收集流程介绍了 CMS 垃圾收集器的工作流程,CMS 收集器的关注点是低延迟,尽可能缩短垃圾收集时用户线程的停顿时间,但在 JDK 9 之后,CMS 不推荐使用。取而代之是 Garbage First,即 G1 垃圾回收器,它除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。

每次根据用户设定允许的收集停顿时间(使用参数 -XX:MaxGCPauseMillis 指定,默认值是200毫秒),优先处理回收价值收益最大的那些 Region, 这也就是 “Garbage First” 名字的由来。这种使用 Region 划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

G1 设计思想

在 G1 收集器出现之前的所有其他收集器,包括 CMS 在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个 Java 堆(Full GC)。而 G1 跳出了这个樊笼,G1 把堆内存分割为很多不相关的区域(Region,物理上不连续区分,逻辑上是连续区分 Eden 区、Survivor区,old区),Region 作为单次回收的最小单元,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC模式。基于 Region 的堆内存布局是它能够实现这个目标的关键,GC 从传统的连续堆内存布局逐渐走向了不连续内存块布局(物理上不连续区分 Eden 区、Survivor区,old区)。
在这里插入图片描述

传统GC堆内存分布

Region:实现可预测的停顿时间模型、Mixed GC 的基础

G1 把堆内存分割为很多不相关的区域,使用不同的 Region 来表示Eden、幸存者0区,幸存者1区,老年代等。Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数 -XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB。 而对于那些超过了整个 Region 容量的超级大对象, 将会被存放在 N 个连续的 Humongous Region 之中, 相加后只要能够确保总大小可以存放这个大对象,就会分配给这个大对象。如果没有能够找到符合条件的连续可用 Region,那么 G1 只能执行一次 Full GC。G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待。

G1 对于大对象有特殊的分配方式。一个大对象是指该对象的大小超过一个 Region 大小的 50% 以上。这个大小包括了Java对象头。

在这里插入图片描述

G1 GC堆内存分布

CSet (Collection Set) :存放待回收的 Region 集合

CSet 代表了在一次 G1 垃圾回收过程中,需要被回收的区域集合。

  • G1 会将一些 Eden 区域和 Survivor 区域(From 区和 To 区)纳入 CSet 进行回收。除了年轻代区域,G1 还会将一些老年代区域纳入 CSet 进行回收。
  • G1 收集器会根据当前堆的使用情况动态调整 CSet 中年轻代区域和老年代区域的比例。通常情况下,CSet 中年轻代区域的比例会较高,因为年轻代区域通常包含更多的垃圾对象。

RSet (Remembered Set):记录对象之间的引用关系

RSet 用于跟踪对象之间的引用关系,是由 G1 收集器维护的一种数据结构,用于记录跨区域的对象引用关系。

  • 每个 Region 区域都会有一个对应的 RSet,记录着该区域中对象引用其他区域中对象的情况。
  • 当应用程序发生对象引用变化时,G1 收集器会实时更新相应区域的 RSet。这个更新过程通常是在应用程序的写屏障(write barrier)中进行的,以确保 RSet 的及时性和准确性。
  • 在 G1 的混合回收过程中,RSet 用于快速定位需要被复制的对象。通过 RSet,G1 可以快速找到跨区域引用的对象,避免全堆扫描,提高回收效率。在 G1 的并发标记过程中,RSet 用于跟踪对象的存活情况,以确定哪些区域需要被回收。

G1 中的 RSet 和 CMS 中 Card Table 作用类似,G1 的 RSet 是在 Card Table 的基础上实现的。G1 GC 的年轻代回收或者混合回收阶段,由于年轻代被尽可能地设计为最大量的回收,这样的设计方式减少了对于 RSet 的依赖,即减弱了对于年轻代里面存储的跟踪引用信息的依赖程度,进而减弱了多余 RSet 的消耗。G1 GC 只在以下两个场景依赖 RSet。

  • 老年代到年轻代的引用:G1 GC 维护了从老年代区间到年轻代区间的指针,这个指针保存在年轻代的 RSet 里面。便于年轻代 GC。
  • 老年代到老年代的引用:G1 GC 维护了从老年代区间到老年代区间的指针,这个指针保存在老年代的 RSet 里面。便于老年代 GC。

RSet 记录了其他 Region 中的对象引用本 Region 中对象的关系,属于points-into 结构(谁引用了我的对象);而 Card Table 则是一种 points-out(我引用了谁的对象)的结构。每个 Region 被分成了多个 Card,在不同 Region 中的 Card会 相互引用。如下图,Region1 中的 Card中 的对象引用了 Region2 中的 Card 中的对象,蓝色实线表示的就是 points-out 的关系,而在 Region2 的 RSe t中,记录了 Region1 的 Card,即红色虚线表示的关系,这就是 points-into。
在这里插入图片描述

三色标记与增量收集

SATB (Snapshot-at-beginning 原始快照):G1 并发标记阶段漏标问题解决方案

漏标问题

并发标记阶段漏标问题,把原本存活的对象错误标记为已消亡。当且仅当以下两个条件同时满足时,会产生 “对象消失” 的漏标问题,即原本应该是黑色的对象被误标为白色:

  1. 已被遍历标记过的黑色对象重新引用了该白色对象;
  2. 删除了全部从灰色对象到该白色对象的直接或间接引用。
    在这里插入图片描述
    CMS 是基于增量更新来做并发标记的。增量更新要破坏的是第一个条件,再以 A 为根节点进行遍历;G1 是基于原始快照来做并发标记的。原始快照要破坏的是第二个条件,再以 B 为根节点进行遍历。

SATB 原始快照介绍

SATB 是 G1 收集器中用于实现并发标记的一种技术,全称为 Snapshot-At-The-Beginning。它的作用是在并发标记过程中保留可达对象的初始快照。

  • 在开始并发标记之前,G1 会先对整个堆内存进行一次初始快照扫描。这个快照会记录下所有可达对象的初始状态。
  • 对象引用变化的记录:在并发标记过程中,如果应用程序修改了对象的引用关系,G1 会通过写屏障记录下这些变化。这些变化会被记录在一个称为 SATB 队列的数据结构中。
  • 在并发标记结束后,G1 会将 SATB 队列中记录的对象引用变化合并到初始快照中。这样可以确保并发标记过程中漏掉的对象能够被正确标记为存活。
  • 增量更新:对于那些在并发标记过程中新创建的对象,G1 会通过增量更新的方式,将它们添加到最终的存活对象集合中。

CMS 并发标记阶段是一个连续的过程,而不是分成多个阶段。G1 并发标记阶段使用增量式标记技术,将整个标记过程划分成多个小步骤,逐步完成标记任务,而不是一次性完成全部标记。这种分阶段进行标记的方式可以提高标记的效率和响应性。

Region 包含了 5 个指针:bottom、previous TAMS、next TAMS、top 和 end。

  • Bottom、End 指针指向 Region 的初始位置与结束位置;
  • Top 指针指向下一个对象分配的内存位置;
  • previous TAMS、next TAMS 指针指定并发标记的区域范围;previous TAMS 指向上一次 TAMS (Top at Mark Start) 的位置;next TAMS 指向下一次 TAMS 的位置。

还需要维护两个用于并发标记的全局 bitmap,分别被标记为 previous 与 next。previous 位图中保存了前一次并发标记的标记结果,next 位图用于保存当前并发标记的标记信息。G1 并发标记阶段使用增量式标记,上一次标记的对象没必要重新遍历,所以本次并发标记阶段可以直接使用上一次已标记的结果,只需标记新一轮的内部快照中的对象。
在这里插入图片描述

各区域含义:

  • [Bottom, End] 区间:该区间代表该 Region 所有的空间。
  • [Top, End] 区间:该区间代表该 Region 还未使用的空间。
  • [Bottom, Top] 区间:该区间代表该 Region 已使用的空间。
  • [Bottom, previous TAMS] 区间:该区间代表上次并发标记完成的区间,该区间能直接被本次正在进行的并发标记使用。
  • [previous TAMS, next TAMS] 区间:该区间代表本次本次正在进行的并发标记的快照区间,next TAMS 为本次并发标记的结束位置。
  • [next TAMS, Top] 区间:该区间代表并发标记过程中,应用线程新生成的对象,在并发标记过程中新分配的对象被认为是存活的对象。

SATB 原始快照算法流程

1、因为是并发标记,应用线程会产生新对象,Top 指针会往后移,所以刚开始时 next TAMS 记录 Top 指针在并发标记开始时的位置,通过 previous TAMS, next TAMS 就得到一个内存快照,指定要并发标记的对象范围为 [previous TAMS, next TAMS];previous TAMS 指向本次快照的起始位置,next TAMS 只想本次快照的结束位置。
在这里插入图片描述

2、在并发标记过程中,应用线程会产生新对象,Top 指针会往后移,
在这里插入图片描述
3、本次并发标记完成,会将 next TAMS 指针的值赋值给 previous TAMS 指针,previous TAMS 便指向了本次快照的结束位置,并作为下次并发标记内存快照的其实位置。
在这里插入图片描述
在并发标记过程中新生成的对象都分配在 [next TAMS, Top] 这块内存区域,G1 算法会将这部分新生成的对象都认为是存活对象,本次标记不会处理这部分新生成对象,留到下一轮标记处理。下一轮并发标记时,Top 指针的值赋值给 next TAMS 指针,与 previous TAMS 形成一个新的内存快照,重复以上流程。

G1 原始快照与 CMS 增量更新对比

  • CMS 的增量更新设计使得它在重新标记阶段必须重新扫描所有线程栈和整个年轻代作为 GC Root;G1 的 SATB 设计在重新标记阶段则只需要扫描 Rest,所以避免了重新标记阶段可能的长耗时。
  • 增量更新关注新增加的引用,把黑色重新标记为灰色,在重新标记中重新扫描;原始快照关注引用的删除,当灰色–>白色之间的引用删除时,只关注白色对象在重新标记中重新扫描。
  • 在处理并发标记阶段引用更新的问题,相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免 CMS 那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。

G1 收集器混合收集流程

G1 过程和 CMS 回收流程有些相似,在 CMS 中遇到的问题(跨代引用)和使用的算法(三色标记)也会在 G1 中出现和运用。G1 的回收过程大致可划分为以下四个步骤。

在这里插入图片描述

1、初始标记 (Initial Marking)

仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程(存在STW),但耗时很短,而且是借用进行 Minor GC 的时候同步完成的。

2、并发标记 (Concurrent Marking)

从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶
段耗时较长,但可与用户程序并发执行,不存在 STW。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。

3、最终标记 (Final Marking)

对用户线程做另一个短暂的暂停(存在 STW),用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。

4、筛选回收 (Live Data Counting and Evacuation)

负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中, 再清理掉整个旧 Region 的全部空间。 这里的操作涉及存活对象的移动,是必须暂停用户线程(存在 STW),由多条收集器线程并行完成的。

从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。从 G1 开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个 Java 堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。

与 CMS 的 “标记-清除” 算法不同,G1 从整体来看是基于“标记-整理” 算法实现的收集器,但从局部(两个 Region 之间)上看又是基于 “标记-复制” 算法实现,无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片。

G1 无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比 CMS 要高。

G1的GC模式

Minor GC、Full GC 都是独占式收集,用户线程暂停(STW),Mixed GC 是并发收集,用户线程和垃圾收集线程并发运行。

年轻代GC(Minor GC/Young GC)

给应用程序分配内存时,当年轻代的 Eden 区用尽时开始年轻代回收过程;G1 的年轻代收集阶段是一个并行的独占收集器。和其他 HotSpot 垃圾收集器一样,当一个年轻代收集进行时,整个年轻代会被回收,所有的应用程序线程会被中断,G1 GC 会启用多线程执行年轻代回收。

年轻代的初始化值默认是整个 Java 堆大小的 5%(通过选项 -XX:G1NewSizePercent 设置),最大值默认是整个 Java 堆大小的 60%(通过选项 -XX:G1MaxNewSizePercent 设置)。每次 GC 的停顿目标时间由选项 -XX:MaxGCPauseMills 设置(默认值是200ms)。如果用户设置了 -Xmn 或者对应的年轻代大小(-XX:NewRatio),那么 G1 GC 会自动忽略该选项设置,进而忽略年轻代大小。

混合GC(Mixed GC)

一旦整个堆空间使用率达到指定的阙值(启动时可配置),G1 会立即启动一个独占的并行初始标记阶段(initial-mark phase)进行标记。由于一个年轻代 GC 必须收集所有的 Roots,所以 G1 的初始标记在一个年轻代GC里完成,即初始标记阶段和年轻代 GC 一起运行。G1 提供了一个选项 -XX:InitiatingHeapOccupancyPercent,默认值是 Java 堆内存空间的 45%,这个选项决定了什么时候初始化并行标记循环,即年轻代 GC 结束之后,G1 会评估剩余的对象是否达到了 45% 这个阙值。(在 CMS GC 里,是单独判断老年代的使用率,而在 G1 GC,判断的是整个 Java 堆的使用率。)

标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC 从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的 G1 回收器和其他 GC 不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的 Region 就可以了。同时,这个老年代 Region 是和年轻代一起被回收的。

G1 收集器混合回收的重要细节:

  • 在并发标记阶段结束后,G1 会识别出老年代中百分之百为垃圾的内存分段进行回收。对于部分为垃圾的内存分段,G1 会计算出其中的垃圾比例,用于后续的混合回收。
  • 混合回收的回收集(Collection Set)包括:老年代中部分被选中的内存分段(通常是八分之一)、Eden 区的内存分段、Survivor 区的内存分段。

Full GC

G1 的 Full GC 采用”标记-整理“算法。Full GC 会对整个 Java 堆进行压缩。G1 在 Full GC 时默认使用 SerialOld 收集器,是单线程的,独占式的(用户线程暂停),会引起较长的停顿时间,因此 G1 的设计目标是减少 Full GC 的发生次数。

G1 触发 Full GC 的场景:

  • 年轻代 GC,拷贝存活对象时,无法找到可用的空闲分区;
  • 老年代 GC,转移存活对象时,无法找到可用的空闲分区;
  • 分配巨型对象时在老年代没有足够的连续可用分区。
  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值