【JVM】二十七、亲身体验Young GC风暴:模拟教程带你走进GC的神秘世界!

本文将通过代码演示来展示年轻代的Young GC是如何发生的

1、前文回顾

在今天的文章,我们将通过代码演示来展示年轻代的Young GC是如何发生的。同时,我们还将指导大家如何在JVM参数中配置打印对应的GC日志。接下来,我们将通过分析GC日志,逐步解析JVM的垃圾回收机制是如何运作的。

2、不可不知的JVM参数设置技巧

首先,根据我们之前的学习,我们知道,在系统运行过程中创建的对象,通常都是优先分配在新生代中的Eden区域。当然,这是针对于非大对象的情况。

具体来说,新生代中除了Eden区域,还有两块名为Survivor的区域。默认情况下,Eden区域占据新生代的80%,而每块Survivor区域则各占据新生代的10%

比如我们用以下JVM参数来运行代码:

-XX:NewSize=5242880 -XX:MaxNewSize=5242880 -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10485760 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC

上述参数是基于JDK 1.8版本进行设置的,不同版本的JDK可能会有不同的参数名称,但它们的基本含义是相似的。

在这段文本中,"-XX:InitialHeapSize"和"-XX:MaxHeapSize"分别代表初始堆大小和最大堆大小。"-XX:NewSize"和"-XX:MaxNewSize"则分别表示初始新生代大小和最大新生代大小。"-XX:PretenureSizeThreshold=10485760"设定了大对象阈值为10MB。

这意味着,堆内存将被分配10MB的内存空间。其中,新生代将占据5MB的内存空间,Eden区将占据4MB,每个Survivor区将占据0.5MB。只有大于10MB的对象才会直接进入老年代。在年轻代中,我们使用ParNew垃圾回收器,而在老年代中,我们使用CMS垃圾回收器。看下图图示。

image-20240422205542600

3、如何轻松捕获GC日志?

接着我们需要在系统的JVM参数中加入GC日志的打印选型,如下所示:
-XX:+PrintGCDetils:打印详细的gc日志
-XX:+PrintGCTimeStamps:这个参数可以打印出来每次GC发生的时间
-Xloggc:gc.log:这个参数可以设置将gc日志写入一个磁盘文件
加上这个参数之后,jvm参数如下所示:

-XX:NewSize=5242880 -XX:MaxNewSize=5242880 -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10485760 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log

4、示例程序代码

接着我们给大家看一段示例程序代码:

public class Demo {
    public static void main(String[] args) {
        byte[] array1 = new byte[1024 * 1024];
        array1 = new byte[1024 * 1024];
        array1 = new byte[1024 * 1024];
        array1 = null;
        byte[] array2 = new byte[2 * 1024 * 1024];
    }
}

5、详解对象在Eden区的奇妙之旅!

这段代码非常简单,首先通过new byte[1024 * 1024]这样的代码连续分配了3个数组,每个数组都是1MB。然后通过局部变量array1依次引用这三个对象,最后还把array1这个局部变量指向了null。那么在JVM中上述代码是如何运行的呢?

首先我们来看第一行代码:byte[] array1 = new byte[1024 * 1024];。这行代码一旦运行,就会在JVM的Eden区内放入一个1MB的对象,同时在main线程的虚拟机栈中会压入一个main()方法的栈帧,在main()方法的栈帧内部,会有一个名为“array1”的变量,这个变量是指向堆内存Eden区的那个1MB的数组。

如下图。

image-20240422210139540

接着我们看第二行代码:array1 = new byte[1024 * 1024];
此时会在堆内存的Eden区中创建第二个数组,并且让局部变量指向第二个数组,然后第一个数组就没人引用了,此时第一个数组就成了没人引用的“垃圾对象”了,如下图所示。

image-20240422214754861

然后看第三行代码:byte[] array1 = new byte[1024 * 1024];。
这行代码在堆内存的Eden区内创建了第三个数组,同时让array1变量指向了第三个数组,此时前面两个数组都没有人引用了,就都成了垃圾对象,如下图所示。

image-20240422214938626

接着我们来看第四行代码:array1 = null;。
这行代码一执行,就让array1这个变量什么都不指向了,此时会导致之前创建的3个数组全部变成垃圾对象,如下图。

image-20240422215040249

在这段代码中,我们可以看到第五行创建了一个大小为2MB的字节数组。这个数组被尝试放入Eden区,但是大家可能会有疑问:Eden区能否容纳这个2MB大小的数组呢?

答案是不能。因为Eden区的总大小只有4MB,而且里面已经放置了3个1MB大小的数组,所以剩余空间只有1MB。在这种情况下,我们无法将一个2MB大小的数组放入Eden区。

因此,当这种情况发生时,会触发年轻代的Young GC。

6、采用指定JVM参数运行程序

之前给大家讲过,在IDEA等开发工具里如何以指定JVM参数运行程序,就是对你的程序右键,然后选择“Run As -> Run Configurations”,接着在下图中填入对应的JVM参数:

image-20240422220018445

然后运行即可,此时运行完毕后,会在下述工程目录中出现一个gc.log文件,里面就是本次程序运行的gc日志,如下图所示。

image-20240422220103280

打开gc.log文件,我们会看到如下所示的gc日志:

Java HotSpot(TM) 64-Bit Server VM (25.361-b09) for windows-amd64 JRE (1.8.0_361-b09), built on Jan  9 2023 08:38:53 by "java_re" with MS VC++ 15.9 (VS2017)
Memory: 4k page, physical 16661748k(8622384k free), swap 19335396k(9495304k free)
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 -XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 
0.202: [GC (Allocation Failure) 0.202: [ParNew: 4044K->512K(4608K), 0.0034758 secs] 4044K->1590K(9728K), 0.0036497 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.238: [GC (Allocation Failure) 0.238: [ParNew: 4028K->506K(4608K), 0.0007688 secs] 5107K->2573K(9728K), 0.0008150 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.244: [GC (Allocation Failure) 0.244: [ParNew (promotion failed): 3657K->3322K(4608K), 0.0004139 secs]0.244: [CMS: 2067K->3714K(5120K), 0.0042231 secs] 5724K->3714K(9728K), [Metaspace: 2744K->2744K(1056768K)], 0.0047194 secs] [Times: user=0.11 sys=0.00, real=0.00 secs] 
0.249: [GC (CMS Initial Mark) [1 CMS-initial-mark: 3714K(5120K)] 7809K(9728K), 0.0002061 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.249: [CMS-concurrent-mark-start]
0.250: [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.250: [CMS-concurrent-preclean-start]
0.251: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.251: [CMS-concurrent-abortable-preclean-start]
0.257: [GC (Allocation Failure) 0.257: [ParNew (promotion failed): 4095K->4095K(4608K), 0.0002735 secs]0.258: [CMS0.258: [CMS-concurrent-abortable-preclean: 0.000/0.007 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
 (concurrent mode failure): 3716K->1329K(5120K), 0.0033578 secs] 7809K->5424K(9728K), [Metaspace: 2746K->2746K(1056768K)], 0.0037087 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
0.261: [Full GC (Allocation Failure) 0.261: [CMS: 1329K->1288K(5120K), 0.0033655 secs] 5424K->5383K(9728K), [Metaspace: 2746K->2746K(1056768K)], 0.0034130 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.265: [GC (Allocation Failure) 0.265: [ParNew: 4096K->0K(4608K), 0.0004298 secs] 5384K->1288K(9728K), 0.0004703 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

您是否觉得这段文本有些混乱,信息密集得让人难以理解?

不用担心,接下来将通过一篇详尽的解析文章,以图文并茂的方式,逐一步骤地分析Young GC(年轻代垃圾回收)的运行机制。在文章中,我们会参照GC日志,帮助大家更清晰地理解Young GC的工作原理。