【JVM】二十二、掌握G1垃圾回收器,让你的在线服务飞起来!
本文将深入介绍G1垃圾回收器,谈谈对象的生命周期,让你的在线服务飞起来!
1、前文回顾
在上篇文章中,我们已经详细探讨了G1垃圾收集器的动态内存管理策略。这种策略的特点是,它会根据当前的运行情况,动态地将内存区域(Region)分配给新生代、Eden区、Survivor区、老年代以及大对象区。
首先,我们需要明确的是,新生代和老年代都有一个各自的最大占比。当新生代的Eden区被填满时,就会触发新生代的垃圾回收机制。
在新生代的垃圾回收过程中,我们采用了复制算法。但是,为了控制垃圾回收的停顿时间,我们引入了一个预设的GC停顿时间。这意味着,我们会根据这个预设时间,选择一部分Region进行垃圾回收,以确保垃圾回收的停顿时间不会超过这个预设时间。
接下来,我们来谈谈对象的生命周期。如果一个对象在新生代中经历了一定次数的垃圾回收,或者触发了动态年龄判定规则,或者存活的对象在Survivor区无法存放,那么这些对象就会被转移到老年代。
另外,对于大对象,我们会将其放入单独的大对象Region,而不是放入老年代。
因此,我们可以看到,在G1垃圾收集器中,新生代的对象会因为各种原因逐渐进入老年代。这是一个动态的、复杂的过程,需要我们根据实际情况进行细致的调整和管理。
2、了解新生代与老年代何时联手进行垃圾回收!
在G1垃圾收集器中,存在一个名为“-XX:InitiatingHeapOccupancyPercent”的参数,其默认值设定为45%。这个参数的含义是,当老年代(Old Generation)占用堆内存的比例达到45%时,系统将尝试触发一次混合回收(Mixed GC)。
以我们之前讨论的堆内存情况为例,假设堆内存总共有2048个Region。如果老年代占据了其中的45%,即接近1000个Region的时候,系统就会开始执行混合回收操作。
这种设计的初衷是为了优化垃圾回收过程。因为在某些情况下,仅仅对新生代(Young Generation)进行垃圾回收可能无法有效释放足够的空间,而需要同时对老年代进行垃圾回收。混合回收可以在一定程度上避免这种情况,提高系统的运行效率。如下图所示。

3、G1垃圾回收解密
首先,会进行一次“初始标记”操作。在这个操作中,系统将进入“Stop the World”状态,目的是快速地标记出所有由GC Roots直接引用的对象。
具体过程如下:
- 暂停程序的运行,确保在标记过程中,对象不会发生变化。
- 对各个线程栈内存中的局部变量(这些变量是GC Roots)以及方法区中的类静态变量(同样也是GC Roots)进行扫描。
- 标记出上述变量直接引用的所有对象。
这个过程通常速度非常快,因为仅仅是标记出直接引用的对象而已。

接着会进入“并发标记”的阶段,这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,如下图所示。

在这里,我们将深入探讨垃圾回收(GC)中的Roots追踪机制。以下面的代码为例,我们可以看到有一个名为User的类,其中包含一个静态变量UserManager,它是一个GC Root对象。在垃圾回收的初始标记阶段,我们只需将UserManager对象标记为与GC Roots直接关联的对象,即它肯定是需要存活的对象。
接着,在并发标记阶段,我们会进行GC Roots追踪。这个过程从UserManager这个GC Root对象开始,沿着与之直接关联的对象进行追踪。在这个过程中,我们发现UserInfosManager对象中有一个实例变量UserInfoFetcher。通过追踪这个UserInfoFetcher变量,我们发现它引用了一个UserInfoFetcher对象。因此,我们需要将这个UserInfoFetcher对象也标记为存活对象。
public class User {
public static UserManager userManager = new UserManager();
}
public class UserManager {
public UserInfoFetcher userInfoFetcher = new UserInfoFetcher();
}
在垃圾回收过程中,并发标记阶段是一个相对耗时的步骤,因为它需要追踪所有的活跃对象。然而,这个阶段可以与系统程序同时运行,因此对系统程序的影响较小。
在并发标记阶段,Java虚拟机(JVM)会记录对象的修改情况,例如哪些对象被创建,哪些对象失去了引用。这些记录有助于在后续阶段更准确地确定哪些对象是活跃的,哪些是垃圾对象。
接下来是最终标记阶段,这个阶段会进入“Stop the World”模式,此时系统程序将暂停运行。在这一阶段,根据并发标记阶段记录的对象修改信息,JVM会对存活对象进行最终标记,以确定哪些对象是垃圾对象。如下图所示。

在垃圾回收的最后一个阶段,即“混合回收”阶段,我们首先会对老年代中的每个Region进行评估。这个评估主要包括计算每个Region中存活对象的数量,以及存活对象所占的比例。同时,我们还会预估执行垃圾回收的预期性能和效率。
在这个基础上,我们会选择暂停系统程序,然后全力以赴地进行垃圾回收。在这个过程中,我们只会选择部分Region进行回收,这是为了确保垃圾回收的停顿时间能够控制在我们预先设定的范围内。
例如,假设老年代中有1000个Region都已经满了,但根据我们的预定目标,本次垃圾回收的停顿时间不能超过200毫秒。那么,通过之前的计算,我们可以得知,回收800个Region恰好需要200毫秒。因此,我们只会回收这800个Region,以确保垃圾回收导致的停顿时间能够控制在我们预设的范围内。如下图。

我们需要明确一点:当老年代的堆内存使用率达到45%时,将触发一种名为“混合回收”的垃圾回收过程。
混合回收,如其名,不仅仅局限于老年代的垃圾清理,它同时涵盖了新生代和大对象区域的垃圾回收。这种策略的引入是为了更好地管理内存,确保系统的稳定性和性能。
具体来说,混合回收会选择哪些特定的区域(Region)进行垃圾回收呢?
这主要取决于我们为垃圾回收设定的停顿时间目标。为了达到这个目标,垃圾回收器会从新生代、老年代和大对象区域中,各自选择一些特定的区域进行回收。这样做的目的是为了在限定的时间内(例如200毫秒)尽可能多地清除垃圾,这就是我们所说的“混合回收”。如下图。

4、G1垃圾回收关键参数详解
在上述描述中,我们了解到了G1垃圾回收器的一些工作原理。当老年代的Region占据堆内存的45%之后,会触发一个混合回收的过程,也就是Mixed GC。这个过程分为好几个阶段,最后一个阶段是执行混合回收,从新生代和老年代里都回收一些Region。
在这个过程中,最后一个阶段混合回收的时候,实际上会停止所有程序运行。因此,G1允许执行多次混合回收。例如,先停止工作,执行一次混合回收回收掉一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。
有一些参数可以控制这个过程,比如“-XX:G1MixedGCCountTarget”参数。这个参数是在一次混合回收的过程中,最后一个阶段执行几次混合回收。默认值是8次,意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反复8次。
假设一次混合回收预期要回收掉一共有160个Region,那么此时第一次混合回收,会回收掉一些Region,比如就是20个Region。

接着恢复系统运行一会儿,然后再执行一次“混合回收”,如下图,再次回收掉20个Region。

在执行了8次混合回收阶段之后,我们不仅完成了预定的160个Region的回收,还将系统停顿时间控制在了指定范围内。
那么,为什么我们需要反复进行多次回收呢?
这是因为,如果我们停止系统一段时间,回收一部分Region,然后让系统运行一段时间,再次停止系统,再回收一部分Region,这样可以有效地避免系统的停顿时间过长。通过这种方式,我们可以在多次回收的间隔中,让系统有机会运行。
此外,还有一个参数需要我们关注,那就是“-XX:G1HeapWastePercent”,其默认值是5%。
这个参数的意义在于,当我们在进行混合回收时,对Region的回收是基于复制算法进行的。具体来说,我们会将需要回收的Region中的活跃对象移动到其他Region,然后清理掉这个Region中的垃圾对象。如下图。

在G1垃圾回收器的回收过程中,新的Region会不断地被释放出来。当空闲的Region数量达到堆内存的5%时,混合回收将会立即停止,这标志着本次混合回收的结束。
从这个机制可以看出,G1垃圾回收器是基于复制算法对Region进行垃圾回收的,因此不会出现内存碎片的问题。这与CMS垃圾回收器不同,后者在标记-清理过程之后,还需要进行内存碎片的整理。
此外,还有一个参数“-XX:G1MixedGCLiveThresholdPercent”,其默认值是85%。这个参数的含义是,在进行回收时,只有存活对象占比低于85%的Region才能被回收。如果一个Region的存活对象超过85%,那么将其回收是没有意义的。在这种情况下,需要将85%的存活对象拷贝到其他Region,而这会带来较高的成本。
5、当Full GC失败时会怎样?
如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去
此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败。
一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。
在执行Mixed回收时,无论是针对年轻代还是老年代,都采用了复制算法进行垃圾回收。这意味着需要将各个Region中存活的对象复制到其他Region中去。
然而,如果在复制过程中发现没有空闲的Region来存放这些存活对象,就会触发一次失败。一旦发生这种情况,系统会立即停止应用程序的运行,并切换到单线程模式。在这种模式下,系统将进行标记、清理和压缩整理操作,以便释放出一部分可用的Region。这个过程是非常缓慢的。