手动模拟发生Young GC

本文将通过设置固定的堆内存、新生代等内存空间大小,写代码去手动触发YoungGC,然后根据打印出的GC log日志去一步一步剖析整个流程。

我们先来设置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

这些参数的解释如下:

  • XX:NewSize和XX:MaxNewSize初始新生代和最大新生代的大小,为5MB
  • XX:InitialHeapSize和XX:MaxHeapSize:初始堆内存大小和最大堆内存大小,为10MB
  • XX:SurvivorRatio:新生代中Eden区和Survivor区的大小比例,8代表Eden区占整个新生代区域的80%
  • XX:PretenureSizeThreshold:指定大对象的阈值是10MB
  • UseParNewGC新生代用的是ParNewGC垃圾回收器
  • XX:+UseConcMarkSweepGC老年代用的是CMS垃圾回收器
  • XX:+PrintGCDetails:打印详细的GC日志
  • XX:+PrintGCTimeStamps:打印出每次GC发生的时间
  • Xloggc:gc.log:将GC日志写入gc.log文件中

相当于给堆内存分配了10MB空间,新生代大小为5MB,其中Eden区占4MB,两个Survivor区分别占0.5MB();老年代大小为5MB,大对象必须超过10MB才直接进入老年代

模拟代码:

public static void main(String[] args) {
byte[] array1 = new byte[1024 * 1024]; //1MB
array1 = new byte[1024 * 1024];//1MB
array1 = new byte[1024 * 1024];//1MB
array1 = null;

byte[] array2 = new byte[2 * 1024 * 1024];
}

代码解析:

  • 代码行1:byte[] array1 = new byte[1024 * 1024]; 在新生代Eden区分配了1MB的空间,用来存储数组:new byte[1024 * 1024],并且在main方法栈内,array1指针指向该数组对象

JVM优化案例实战-手动模拟Young GC_sed

\

\

  • 代码行2:array1 = new byte[1024 * 1024]; 在新生代Eden区又分配了1MB的空间,用来存储数组:new byte[1024 * 1024],并且array1指向新的数组,原来的数组成为了垃圾对象

JVM优化案例实战-手动模拟Young GC_堆内存_02

\

  • 代码行3:array1 = new byte[1024 * 1024]; 又重新分配了一个数组空间,并且array1指向最新的数组对象,此时Eden区内有两个垃圾对象

JVM优化案例实战-手动模拟Young GC_堆内存_03

\

  • 代码行4:array1 = null; array1不指向任何对象,则之前分配的三个数组对象都成为垃圾对象
  • 代码行5:byte[] array2 = new byte[2 * 1024 * 1024]; 在Eden区内分配2MB的空间。但是由于之前在Eden区已经有三个对象,占用了3MB空间,Eden本身只有4MB,需要再分配2MB很明显空间不够,会触发Young GC

执行代码后,会在目录下生成gc.log日志文件,我们可以根据这个文件查看GC回收的具体细节,这是我们做JVM调优的基础。

GC日志解析

GC日志如下:

Java HotSpot(TM) 64-Bit Server VM (25.321-b07) for bsd-amd64 JRE (1.8.0_321-b07), built on Dec 15 2021 19:12:29 by "java_re" with gcc 4.2.1 Compatible Apple LLVM 11.0.0 (clang-1100.0.33.17)
Memory: 4k page, physical 16777216k(45636k free)

/proc/meminfo: 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:+UseParNewGC
0.125: [GC (Allocation Failure) 0.125: [ParNew: 3596K->420K(4608K), 0.0047305 secs] 3596K->1446K(9728K), 0.0053325 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] Heap
par new generation total 4608K, used 3642K [0x00000007bf600000, 0x00000007bfb00000, 0x00000007bfb00000) eden space 4096K, 78% used [0x00000007bf600000, 0x00000007bf925a40, 0x00000007bfa00000) from space 512K, 82% used [0x00000007bfa80000, 0x00000007bfae9048, 0x00000007bfb00000) to space 512K, 0% used [0x00000007bfa00000, 0x00000007bfa00000, 0x00000007bfa80000) concurrent mark-sweep generation total 5120K, used 1026K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000) Metaspace used 3216K, capacity 4496K, committed 4864K, reserved 1056768K class space used 358K, capacity 388K, committed 512K, reserved 1048576K

下面我们就来分析下GC日志。

CommandLine flags

本次代码执行的JVM参数,其中有我们设置的JVM参数,也有系统默认的设置参数

Young GC:
0.125: [GC (Allocation Failure) 0.125: [ParNew: 3596K->420K(4608K), 0.0047305 secs] 3596K->1446K(9728K), 0.0053325 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

这一行是我们需要分析的重点。

0.125: [GC (Allocation Failure) 0.125 表示在系统运行0.125S后,内存分配失败,为什么会内存分配失败呢?看我们之前的代码,Eden区总的4MB空间,之前已经有3个垃圾对象,最后需要分配2MB的空间,就会发生内存不够,因此分配失败,此时就会触发一次Young GC,

[ParNew: 3596K->420K(4608K), 0.0047305 secs]

ParNew表示使用的是年轻代的ParNew 垃圾回收器来进行垃圾回收,后面的3596K->420K表示,回收之前的新生代大小为3.5MB,回收之后为420K约等于0.5MB,相当于本次回收大概回收了3MB的垃圾对象。0.0047305 secs表示本次GC回收耗时。

新生代区的总可用大小为4MB+0.5MB=4.5MB,4.5MB呢就是Eden区大小+一个Survivor区的大小,因为两个Survivor区一个需要用来存放存活对象,另一个必须保持空闲,所以总可用大小包括了Eden区大小+空闲Survivor区大小。

3596K->1446K(9728K), 0.0053325 secs]表示整个堆内存空间的使用情况,总的大小为9728K也就是9.5MB,为什么不是10MB呢 ?其实就是去除了一个Survivor区域的内存空间。在进行GC回收之前堆内存空间是3596K,回收后是1446K。

Heap:
Heap
par new generation total 4608K, used 3642K [0x00000007bf600000, 0x00000007bfb00000, 0x00000007bfb00000)
eden space 4096K, 78% used [0x00000007bf600000, 0x00000007bf925a40, 0x00000007bfa00000)
from space 512K, 82% used [0x00000007bfa80000, 0x00000007bfae9048, 0x00000007bfb00000)
to space 512K, 0% used [0x00000007bfa00000, 0x00000007bfa00000, 0x00000007bfa80000)
concurrent mark-sweep generation total 5120K, used 1026K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000)
Metaspace used 3216K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 358K, capacity 388K, committed 512K, reserved 1048576K

这些日志是在JVM退出前打印出的当时JVM堆内存使用的情况。

包括使用ParNew GC的新生代、Eden、From Survivor区、To Survivor区、使用CMS的老年代、元空间等的内存使用情况。