【JVM】二十一、G1分代回收究竟如何让传统方法黯然失色?

本文将深入介绍G1垃圾回收器,了解G1分代回收究竟如何让传统方法黯然失色的

1、前文回顾

在上一篇文章中,我们详细解析了G1垃圾回收器的设计思想。其核心理念在于将内存分割为众多小的Region,并针对新生代和老年代各自分配一部分Region。在垃圾回收过程中,G1会优先挑选那些能实现最短停顿时间以及最多回收对象的Region,以尽可能确保达到预设的垃圾回收系统停顿时间。

在这篇文章中,我们将深入探讨G1垃圾回收器的工作流程。从对象在内存中的分配,到垃圾回收的触发,我们将逐步分析,帮助大家更好地理解G1垃圾回收器的工作原理。

2、如何为G1配置最佳内存量

大家看如下的图,我们都知道G1对应的是一大堆的Region内存区域,每个Region的大小是一致的。

image-20240421175724055

首先,我们需要思考两个问题:有多少个Region?每个Region的大小是多少?

在默认情况下,这些值是自动计算和设置的。我们可以为整个堆内存设置一个大小,例如使用“-Xms”和“-Xmx”来设置堆内存的大小。当JVM启动时,如果发现使用的是G1垃圾回收器(可以通过“-XX:+UseG1GC”参数指定),它会自动将堆大小除以2048,因为JVM最多可以有2048个Region。同时,Region的大小必须是2的倍数,例如1MB、2MB、4MB等。

以堆大小为4GB(即4096MB)为例,将其除以2048个Region,每个Region的大小就是2MB。这就是决定Region数量和大小的方式,通常保持默认计算方式即可。

当然,也可以通过手动方式来指定,使用“-XX:G1HeapRegionSize”参数进行设置。如下图。

image-20240421175952599

在JVM的G1垃圾收集器中,新生代堆内存的初始占比默认为5%,这意味着它大约占200MB的内存。这个比例大约对应于100个Region。这个初始占比可以通过"-XX:G1NewSizePercent"参数来设置。然而,通常情况下,保持这个默认值就足够了。

在系统运行过程中,JVM会动态地增加新生代的Region数量,但是新生代的最大占比不会超过60%,这可以通过"-XX:G1MaxNewSizePercent"参数进行限制。

此外,当一个Region进行了垃圾回收后,新生代的Region数量可能会减少,这都是JVM的动态调整过程。

从下面的图可以看出,一开始,只有一部分的Region被划分为新生代。

image-20240421180131746

3、新生代对Eden和Survivor概念的重新定义

在G1垃圾收集器中,尽管内存被划分为多个Region,但实际上仍然存在新生代和老年代的区分。在新生代中,还进一步分为Eden区和Survivor区。因此,我们会发现之前学到的许多技术原理在G1时代仍然适用。

大家可能还记得之前提到过的一个关于新生代的参数:“-XX:SurvivorRatio=8”。这意味着我们仍然可以区分出属于新生代的Region中哪些是Eden区,哪些是Survivor区。

例如,在新生代初始时,如果有100个Region,那么可能有80个Region属于Eden区,两个Survivor区分别占据10个Region。如下图。

image-20240421180311147

在理解JVM的内存管理时,我们需要明白,其实在这里,Eden区和Survivor区的概念是存在的,并且它们会各自占据不同的Region。

当我们在新生代中不断分配对象时,属于新生代的Region会不断增加。这是因为,随着对象的不断分配,JVM会为新的Region分配空间。同时,Eden区和Survivor区对应的Region也会相应地增加。

4、揭秘G1新生代垃圾回收的奥秘

由于G1的新生代同样存在Eden和Survivor的划分,因此,垃圾回收的触发机制与之前是相似的。

在不断向新生代的Eden区域对应的Region中放入对象的过程中,JVM会持续为新生代增加更多的Region,直至新生代占据堆内存的最大比例,即60%。

当新生代达到设定的占据堆内存最大比例60%时,比如已经包含1200个Region,其中,Eden可能占据了1000个Region,每个Survivor则占据了100个Region。此时,如果Eden区的对象也已经完全占满,那么情况将如下图所示。

image-20240421180522217

在这个阶段,新生代的垃圾回收(GC)将会被触发。G1垃圾回收器会采用我们之前讨论过的复制算法来执行垃圾回收操作。在这个过程中,系统会进入一个“Stop the World”状态。

在这个状态下,G1垃圾回收器会将Eden区域中的存活对象移动到S1对应的Region中。然后,它会自动清理Eden区域中的垃圾对象,以释放空间。这个过程可以通过以下图像来理解。

image-20240421180644023

然而,这个过程中存在一些差异,因为G1垃圾回收器允许我们设定目标的GC停顿时间。这意味着G1在执行垃圾回收时,可以设置系统的最大停顿时间。我们可以通过“-XX:MaxGCPauseMills”参数来设定这个值,其默认值是200毫秒。

基于这个设定,G1会通过之前提到的方法,对每个Region进行追踪,计算回收所需的时间和可回收的对象数量。然后,它会选择合适的Region进行部分回收,以确保垃圾回收的停顿时间控制在设定的范围内,并尽可能多地回收对象。

这种策略使得G1能够在控制垃圾回收的停顿时间的同时,尽可能地提高对象的回收率。

5、对象什么时候进入老年代?

在G1垃圾回收器的内存模型中,堆内存被划分为多个区域(Region),其中新生代和老年代各自占有一部分。默认情况下,新生代最多可以占据堆内存的60%的区域,而老年代则最多可以占据40%的区域,约为800个区域。

那么,对象何时会从新生代晋升到老年代呢?这个过程与之前的描述基本相同,主要涉及以下几个条件:

  1. 对象在新生代中经历了多次垃圾回收,达到了一定的年龄。这个年龄可以通过"-XX:MaxTenuringThreshold"参数进行设置。当对象的年龄达到这个阈值时,它将被移动到老年代。

  2. 动态年龄判定规则:如果在一次新生代垃圾回收后,存活的对象数量超过了Survivor区域的50%,则会触发动态年龄判定。例如,如果1岁、2岁、3岁和4岁的对象的大小总和超过了Survivor的50%,那么所有年龄大于等于4岁的对象都将被移动到老年代。这就是动态年龄判定规则。

通过下图可以看出,经过一段时间的新生代使用和垃圾回收后,总会有一些对象被晋升到老年代。

image-20240421180807461

6、大对象Region

在当前的讨论中,大家可能会产生一个疑问:之前我们提到大对象可以直接进入老年代,那么在G1垃圾收集器的内存模型下,情况是如何的呢?

实际上,G1垃圾收集器在这个问题上做了一些改变。G1垃圾收集器提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。

在G1垃圾收集器中,判断一个大对象的标准是,如果一个大对象的大小超过了一个Region大小的50%,那么这个大对象就会被放入专门用于存放大对象的Region中。例如,如果我们按照上面的例子,每个Region的大小是2MB,只要一个大对象的大小超过了1MB,那么这个大对象就会被放入专门用于存放大对象的Region中。

此外,如果一个大对象的大小非常大,它可能会横跨多个Region进行存储。如下图所展示的那样。

image-20240421181012691

在G1垃圾回收器中,堆内存被划分为多个Region,其中60%的Region用于存放新生代对象,40%的Region用于存放老年代对象。那么,大对象是如何存放的呢?

首先,我们需要明确的是,在G1垃圾回收器中,新生代和老年代的Region是在不断变化的。例如,假设新生代现在占据了1200个Region,但在一次垃圾回收之后,可能有1000个Region变得空了。这时,这1000个Region就不再属于新生代,而是可以被用来存放大对象。

接下来,我们可能会问,既然大对象不属于新生代和老年代,那么何时会触发对这些大对象的垃圾回收呢?

其实,当新生代和老年代进行垃圾回收时,大对象Region也会被一同回收。这就是在G1内存模型下,大对象的分配和回收策略。