JVM实战—7.如何模拟GC场景并阅读GC日志

news2025/1/4 2:28:28

大纲

1.动手模拟出频繁Young GC的场景

2.JVM的Young GC日志应该怎么看

3.代码模拟动态年龄判定规则进入老年代

4.代码模拟S区放不下部分进入老年代

5.JVM的Full GC日志应该怎么看

6.问题汇总

1.动手模拟出频繁Young GC的场景

(1)程序的JVM参数示范

(2)如何打印出JVM GC日志

(3)示例程序代码

(4)对象是如何分配在Eden区内的

(5)采用指定JVM参数运行程序

(1)程序的JVM参数示范

平时我们系统运行创建的对象,通常都优先分配在新生代中的Eden区。除非是大对象,大对象会直接进入老年代或者大对象专属Region区域。新生代有两块S区,默认Eden区占新生代80%,每块S区占新生代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指定了大对象阈值是10M;

上面的JVM参数,相当于是给堆内存分配10M内存空间。其中新生代是5M内存空间,Eden区占4M,每个Survivor区占0.5M。大对象必须超过10M才会直接进入老年代,年轻代使用ParNew垃圾回收器,老年代使用CMS垃圾回收器。如下图示:

图片

(2)如何打印出JVM GC日志

接着需要在系统的JVM参数中加入GC日志的打印选型,如下所示:​​​​​​​

-XX:+PrintGCDetils:打印详细的GC日志;-XX:+PrintGCTimeStamps:这个参数可以打印出每次GC发生的时间;-Xloggc:gc.log:这个参数可以设置将GC日志写入一个磁盘文件;

加上打印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

(3)示例程序代码​​​​​​​

public class Demo1 {    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];    }}

(4)对象是如何分配在Eden区内的

上述代码先通过new byte[1024 * 1024]连续分配3个数组,每个数组1M;然后通过array1这个局部变量依次引用这三个对象;最后还把array1这个局部变量指向了null。

那么在JVM中上述代码是如何运行的呢?

一.首先来看第一行代码:byte[] array1 = new byte[1024 * 1024];

这行代码一旦运行,就会在JVM的Eden区内放入一个1M的对象,同时在main线程的虚拟机栈中会压入一个main()方法的栈帧。在main()方法的栈帧内部,会有一个名为array1的局部变量。这个array1局部变量指向了堆内存Eden区的那个1M的数组,如下图示:

图片

二.接着看第二行代码:array1 = new byte[1024 * 1024];

这行代码会在Eden区创建第二个数组,并让array1变量指向第二个数组。然后第一个数组就没被引用了,成了垃圾对象。如下图示:

图片

三.然后看第三行代码:array1 = new byte[1024 * 1024];

这行代码会在Eden区创建第三个数组,并让array1变量指向第三个数组。此时前面两个数组都没有被引用了,都成了垃圾对象,如下图示:

图片

四.接着看第四行代码:array1 = null;

这行代码一执行,就会让array1局部变量什么都不指向了,此时会导致之前创建的3个数组全部变成垃圾对象。如下图示:

图片

五.最后看第五行代码:byte[] array2 = new byte[2 * 1024 * 1024];

此时会分配一个2M大小的数组,尝试放入Eden区中。这时Eden区明显已经不能放下这个数组了,因为Eden区总共4M,里面已经放入3个1M的数组,剩余空间只有1M。此时再放一个2M的数组是放不下的,所以这个时候就会触发年轻代的Young GC;

(5)采用指定JVM参数运行程序

图片

图片

图片

图片

图片

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

图片

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

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.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 par new generation   total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
  eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)
  from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)
  to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
 concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 300K, capacity 386K, committed 512K, reserved 1048576K

2.JVM的Young GC日志应该怎么看

(1)程序运行采用的默认JVM参数如何查看

(2)一次GC的概要说明

(3)图解GC执行过程

(4)GC过后的堆内存使用情况

(5)Metaspace中的capacity、committed、reserved

(1)程序运行采用的默认JVM参数如何查看

在GC日志中,可以看到如下内容:​​​​​​​

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

这展示了这次运行程序采取的JVM参数,包括设置的和默认的参数。

所以如果没有设置JVM参数,应该怎么看系统默认使用的JVM参数?

方法就是:给JVM加一段打印GC日志的参数,这样在GC日志里就可以看到默认给JVM进程分配多大的内存空间了。

(2)一次GC的概要说明

接着看GC日志中的如下一行:该行日志概要说明了本次GC的执行情况。

0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

一.为什么会发生一次GC

从上图可知,因为最后要分配一个2M的数组,结果Eden区内存不够。所以就出现了GC (Allocation Failure),也就是对象分配失败,所以此时就要触发一次Young GC。

二.这次GC是什么时候发生的

很简单,看一个数字——0.268,这指的是系统运行后过了多少秒就发生本次GC。比如这里的日志就是系统运行后过了268毫秒就发生了本次GC。

三.新生代GC使用的是ParNew

ParNew: 4030K->512K(4608K)。ParNew的意思是:由于这次触发的是Young GC,于是用指定的ParNew垃圾回收器执行。

四.GC前新生代使用空间、GC后存活对象、新生代可用空间

ParNew: 4030K->512K(4608K)。这个代表的意思是:新生代可用空间是4608K,也就是4.5M。为什么新生代可用空间是4608K?因为Eden区是4M,两个S区只有一个可放存活对象,另一个要保持空闲。所以新生代的可用空间就是Eden区 + 1个S区的大小,即4608K = 4.5M。

然后4030K->512K,表示的是对新生代执行了一次GC。GC前已经使用了4030K,但是GC后只有512K的对象存活下来。

五.本次新生代GC消耗时间,单位精确到微妙

0.0015734 secs。这个就是本次新生代GC耗费的时间,大概耗费1.5ms,回收3M的对象。

六.GC前和GC后的Java堆内存使用了多少

4030K->574K(9728K), 0.0017518 secs。这个代表的是整个Java堆内存的情况,整个Java堆内存的总可用空间是9728K = 新生代4.5M + 老年代5M。GC前整个Java堆内存使用了4030K,GC后Java堆内存使用了574K。

七.本次GC消耗时间,单位到精确到10毫秒

[Times: user=0.00 sys=0.00, real=0.00 secs]。这个代表的是本次GC消耗的时间,这里最小单位是小数点之后两位。这里全部是0.00 secs,也就是本次GC就耗费了几毫秒,所以是0.00s。

(3)图解GC执行过程

第一:看这行日志

ParNew: 4030K->512K(4608K), 0.0015734 secs。在GC前,明明在Eden区只放了3个1M的数组,大小一共是3M=3072K。那么GC前新生代应该是使用了3072K,为什么显示使用了4030K内存?

对于这个问题,需要明白两点:

一.虽然创建的数组是1M,但为了存储它,JVM还会附带一些其他信息。所以每个数组实际占用的内存是大于1M的。

二.除了自己创建的对象以外,可能还有一些看不见的对象在Eden区。至于这些看不见的未知对象是什么,可通过专门的工具分析堆内存快照。

所以如下图示:GC前三个数组和其他一些未知对象加起来,就是占据了4030K的内存。

图片

接着想要在Eden区分配一个2M的数组,此时就会触发Allocation Failure。Allocation Failure表示对象分配失败,于是触发Young GC。然后使用ParNew垃圾回收器进行垃圾回收,回收掉之前创建的三个数组。因为此时这三个数组都没被引用,而成为垃圾对象了。如下图示:

图片

第二:看这行日志

ParNew: 4030K->512K(4608K), 0.0015734 secs

新生代GC回收后,新生代中已使用的内存从4030K降低到了512K。也就是说这次YGC有512K的对象存活下来,从Eden区转移到了S区。如下图示:

图片

以上就是本次GC的全过程。

(4)GC过后的堆内存使用情况

接着看下面的GC日志,这段日志是在JVM退出时打印出来的当前堆内存的使用情况。​​​​​​​

Heap
 par new generation total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
  eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)
  from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)
  to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
 concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 300K, capacity 386K, committed 512K, reserved 1048576K

一.当前新生代总共可用内存和已经使用内存

par new generation total 4608K, used 2601K

这代表ParNew垃圾回收器负责的新生代总共有4608K(4.5M)可用内存,目前已经使用了的内存是2601K(2.5M)。此时在JVM退出前,为什么新生代占用了2.5M的内存?

因为在GC后,会通过如下代码又分配一个2M的数组:byte[] array2 = new byte[2 * 1024 * 1024];

所以此时在Eden区中一定会有一个2M的数组,也就是2048K。然后上次GC后在Survivor From区中存活了512K的对象。由于每个数组会额外占据一些内存来存放一些自己这个对象的元数据,所以可以认为多出来的41K是数组对象额外使用的内存空间。因此GC后新生代占用的大小是:2048K + 512K + 41K = 2601K。如下图示:

图片

二.接着看以下GC日志​​​​​​​

 eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)
 from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)
 to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)

其中清晰地表示,此时:Eden区4M的内存被使用了51%,因为有一个2M的数组在里面。然后Survivor From区,显示512K,100%的使用率。也就是Survivor From区被这次GC后存活下来的512K对象全占据了。

三.接着看以下GC日志

如下日志指的是CMS管理的老年代内存空间一共5M,此时已用62K。

concurrent mark-sweep generation total 5120K, used 62K,

而下面两段日志的意思是:Metaspace元数据空间和Class空间的总容量、使用内存等,Metaspace元数据空间会存放一些类的信息、常量池信息等。​​​​​​​

Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 300K, capacity 386K, committed 512K, reserved 1048576K

(5)Metaspace中的capacity、committed、reserved

Java8取消了永久代PermGen,取而代之的是元数据区MetaSpace。方法区在Java8以后移至元数据区MetaSpace,JDK8开始把类的元数据放到本地内存(Native Heap),称为MetaSpace。理论上本地内存剩余多少,MetaSpace就可以有多大。

当然我们不可能无限制增大MetaSpace,需要用-XX:MaxMetaSpaceSize指定MetaSpace大小。

关于used capacity commited和reserved:MetaSpace由一个或多个Virtual Space(虚拟空间)组成。虚拟空间是操作系统的连续存储空间,虚拟空间是按需分配的。分配时,虚拟空间会向OS预留(reserve)空间,但还没被提交(commit)。

一.MetaSpace的预留空间(reserved)是全部虚拟空间的大小

虚拟空间的最小分配单元是MetaChunk(也可以说是Chunk),当新的Chunk被分配至虚拟空间时,与Chunk相关的内存空间会被提交。

二.MetaSpace的committed指的是所有Chunk占有的空间

每个Chunk占据空间不同,当一个类加载器(Class Loader)被GC时:所有与之关联的Chunk被释放(freed),这些释放的Chunk会维护在一个全局的释放数组里。

三.MetaSpace的capacity指的是所有未被释放的Chunk占据的空间

假如从GC日志发现committed是4864K,capacity4486K。说明有一部分的Chunk已经被释放了,代表有类加载器被回收了。附上原文链接:

https://stackoverflow.com/questions/40891433/understanding-metaspace-line-in-jvm-heap-printout

3.代码模拟动态年龄判定规则进入老年代

(1)动态年龄判定规则

(2)动态年龄判定规则的示例代码

(3)示例代码运行后产生的GC日志

(4)GC日志分析

(5)完善示例代码

(6)分析最终版的GC日志 

(7)总结

(1)动态年龄判定规则

对象进入老年代的四个常见时机如下:

一.躲过15次新生代GC后(年龄达到15岁)

二.动态年龄判定规则

如果在S区内,年龄1+年龄2+年龄3+年龄n的对象总和大于S区的50%。此时年龄n及以上的对象会进入老年代,不一定需要达到15岁。所以动态年龄判断规则有个推论:如果S区中的同龄对象大小超过S区内存的一半,就要直接升入老年代。

三.如果一次YGC后存活对象太多无法放入S区,就会直接放入老年代

四.大对象直接进入老年代

首先通过代码模拟出最常见的一种进入老年代的情况:如果S区内年龄1 + 年龄2 + 年龄3 + 年龄n的对象总和大于S区的50%,此时年龄n及以上的对象会进入老年代,也就是动态年龄判定规则。

示例程序的JVM参数如下:​​​​​​​

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

在这些参数里需要注意几点:

Java堆的总大小通过-XX:InitialHeapSize设置为20M,新生代通过-XX:NewSize设置为10M,所以老年代是10M。然后通过-XX:SurvivorRatio参数可知,Eden区是8M,每个S区是1M。接着大对象必须超过10M才会直接进入老年代,-XX:MaxTenuringThreshold=15设置对象年龄达到15岁会进入老年代。

一切准备就绪,先看当前的内存分配情况:

图片

(2)动态年龄判定规则的示例代码​​​​​​​

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

接下来运行示例代码,然后通过打印出的GC日志分析上述代码执行后JVM中的对象分配情况。

(3)示例代码运行后产生的GC日志

把上述示例代码以及给出的JVM参数配合起来运行,此时会看到如下的GC日志。

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 
  -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 
  -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails 
  -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers 
  -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
0.297: [GC (Allocation Failure) 0.297: [ParNew: 7260K->715K(9216K), 0.0012641 secs] 7260K->715K(19456K), 0.0015046 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 par new generation   total 9216K, used 2845K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,  69% used [0x00000000ff500000, 0x00000000ff5b2e10, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 concurrent mark-sweep generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
 class space    used 300K, capacity 386K, committed 512K, reserved 1048576K

(4)GC日志分析

一.首先看如下几行代码​​​​​​​

byte[] array1 = new byte[2 * 1024 * 1024];array1 = new byte[2 * 1024 * 1024];array1 = new byte[2 * 1024 * 1024];array1 = null;

这里连续创建了3个2M的数组,最后还把局部变量array1设置为null,所以此时的内存如下图示:

图片

二.接着执行下面这行代码:

byte[] array2 = new byte[128 * 1024];

此时会在Eden区创建一个128K的数组同时由array2局部变量来引用,如下图示:

图片

三.然后会执行下面的代码:

byte[] array3 = new byte[2 * 1024 * 1024];

此时希望在Eden区再次分配一个2M的数组,由于此时Eden区里已有3个2M数组和1个128K数组,大小都超过6M了。Eden区总共才8M,此时是不可能在Eden区再次分配一个2M的数组的,因此一定会触发一次Young GC。

四.接着开始看GC日志:

ParNew: 7260K->715K(9216K), 0.0012641 secs

这行日志清晰表明,在GC前新生代占用了7260K的内存。大概就是6M的3个数组 + 128K的1个数组 + 几百K的未知对象 = 7260K,如下图示:

图片

五.接着看回上述GC日志:

7260K->715K(9216K)

这表明,一次Young GC过后,剩余的存活对象是715K。由于新生代刚开始会有512K左右的未知对象,此时再加上128K的数组,差不多就是715K。

六.接着看如下GC日志:​​​​​​​

par new generation   total 9216K, used 2845K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
 from space 1024K,  69% used [0x00000000ff500000, 0x00000000ff5b2e10, 0x00000000ff600000)
 to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
concurrent mark-sweep generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

从上面的日志可以清晰看出:此时From Survivor区域被占据了69%的内存,大概就是700K左右。这就是一次Young GC后存活下来的对象,它们都进入From Survivor区。

同时Eden区域内被占据了26%的空间,大概就是2M左右。这就是执行代码"byte[] array3 = new byte[2 * 1024 * 1024]"时,在Young GC后分配在Eden区内的数组。如下图示:

图片

现在Survivor From区里的那700K对象,是1岁。它们熬过一次GC,年龄就会增长1岁。此时S区总大小是1M,存活对象已经有700K了,已经超过了50%。

(5)完善示例代码​​​​​​

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

把示例代码给完善一下变成上述的样子,接下来要触发第二次YGC,然后看S区内的动态年龄判定规则能否生效。

一.接着前面代码执行的分析,继续看如下代码:​​​​​​​

array3 = new byte[2 * 1024 * 1024];array3 = new byte[2 * 1024 * 1024];array3 = new byte[128 * 1024];array3 = null;

这几行代码运行后,会接着分配2个2M的数组。然后再分配一个128K的数组,最后让array3变量指向null。如下图示:

图片

二.此时接着会运行下面的代码:

byte[] array4 = new byte[2 * 1024 * 1024];

这时会发现,Eden区如果要再放一个2M数组进去,是放不下的,所以此时会触发一次YGC。使用上述JVM参数运行这段程序会看到如下GC日志:​​​​​​​

0.269: [GC (Allocation Failure) 0.269: [ParNew: 7260K->713K(9216K), 0.0013103 secs] 7260K->713K(19456K), 0.0015501 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.271: [GC (Allocation Failure) 0.271: [ParNew: 7017K->0K(9216K), 0.0036521 secs] 7017K->700K(19456K), 0.0037342 secs] [Times: user=0.06 sys=0.00, real=0.00 secs]
Heap
par new generation   total 9216K, used 2212K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 eden space 8192K,  27% used [0x00000000fec00000, 0x00000000fee290e0, 0x00000000ff400000)
 from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 700K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
 class space    used 300K, capacity 386K, committed 512K, reserved 1048576K

(6)分析最终版的GC日志

一.首先第一次GC的日志如下:

0.269: [GC (Allocation Failure) 0.269: [ParNew: 7260K->713K(9216K), 0.0013103 secs] 7260K->713K(19456K), 0.0015501 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

这个过程前面已经分析过了。

二.接着第二次GC的日志如下:

0.271: [GC (Allocation Failure) 0.271: [ParNew: 7017K->0K(9216K), 0.0036521 secs] 7017K->700K(19456K), 0.0037342 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] 

第二次触发Yuong GC,就是第一次赋值给局部变量array4的时候。此时的日志"ParNew: 7017K->0K(9216K)"表明:这次GC过后,新生代直接就没有对象了。但array2这个局部变量还一直引用一个128K的数组,它是存活对象。那么这128K的数组以及还有那500多K的未知对象,此时都去哪里了?

首先在Eden区里的3个2M的数组和1个128K的数组,肯定会被回收掉的。如下图示:

图片

然后发现S区中的对象都是存活的,且总大小超过50%以及年龄都是1岁。于是根据动态年龄判定规则:年龄1+...年龄n的对象总大小超S区50%,年龄n及以上的对象进老年代。由于此时S区里的对象的年龄都是1,所以会全部直接进入老年代了。

S区的对象第一次YGC进来时已超50%,但在第二次YGC还存活才升代。所以不是进入S区的时候使用动态年龄去判断,而是扫描S区时才去判断。

如下图示:

图片

这个可以从下面的日志进行确认:

concurrent mark-sweep generation total 10240K, used 700K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

CMS管理的老年代,此时使用空间刚好是700K。由此证明此时Survivor的对象触发了动态年龄判定规则,虽然没达到15岁,但全部进入老年代了。

三.然后array4变量引用的2M的数组,此时就会分配到Eden区中,如下图示:

图片

看如下日志:

eden space 8192K,  27% used [0x00000000fec00000, 0x00000000fee290e0, 0x00000000ff400000)

这就说明Eden区当前就是有一个2M的数组,然后再看下面的日志:​​​​​​​

from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)

表示两个Survivor区域都是空的,因为之前存活的700K对象都进入老年代了,所以现在Survivor区都空了。

(7)总结

这里分析了对象是如何通过动态年龄判定规则进入老年代的。如果每次YGC过后存活的对象太多,特别是超过了S区50%的空间。那么下次YGC时就会触发动态年龄判定规则让一些对象进入老年代中。

注意:不是进入S区的时候就用动态年龄去判断,而是扫描S区时才判断。

4.代码模拟S区放不下部分进入老年代

(1)示例代码

(2)GC日志

(3)分析GC日志

(4)总结

(1)示例代码​​​​​​​

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

(2)GC日志

使用之前的JVM参数来跑一下上面的程序,可以看到下面的GC日志:​​​​​​​

0.421: [GC (Allocation Failure) 0.421: [ParNew: 7260K->573K(9216K), 0.0024098 secs] 7260K->2623K(19456K), 0.0026802 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 par new generation   total 9216K, used 2703K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,  55% used [0x00000000ff500000, 0x00000000ff58f570, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 concurrent mark-sweep generation total 10240K, used 2050K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
 class space    used 300K, capacity 386K, committed 512K, reserved 1048576K 

(3)分析GC日志

一.首先看如下几行代码:​​​​​​​

byte[] array1 = new byte[2 * 1024 * 1024];
array1 = new byte[2 * 1024 * 1024];
array1 = new byte[2 * 1024 * 1024];

byte[] array2 = new byte[128 * 1024];
array2 = null;

上面的代码中:首先分配了3个2M的数组,然后让array1变量指向第三个2M数组。接着创建了一个128K的数组,并让array2指向了null。同时我们一直都知道,Eden区里会有500K左右的未知对象,如下图示:

图片

二.接着会执行如下代码:

byte[] array3 = new byte[2 * 1024 * 1024];

此时想要在Eden区里再创建一个2M的数组,肯定是不行的,所以此时必然会触发一次Young GC。

先看如下日志:

ParNew: 7260K->573K(9216K), 0.0024098 secs

这里清晰说明了,本次GC过后,新生代里就剩下了500多K的对象了。这是为什么呢,此时明明array1变量是引用了一个2M的数组的。

因为这次GC时,会回收掉上图中的2个2M的数组和1个128K的数组,然后留下一个2M的数组和1个未知的500K的对象作为存活对象。这时存活下来的2M数组和500K未知对象是不能放入Survivor区的,因为Survivor区只有1M。如下图示:

图片

三.根据对象进入老年代规则,此时是否要把全部存活对象都放入老年代

也不是,因为首先根据如下日志:

eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)

可知,Eden区内一定放入了一个新的2M的数组,而且这个数组就是刚才最后想要分配的那个数组,由array3变量引用。此时会如下图示:

图片

然后再根据如下的日志:

from space 1024K,  55% used [0x00000000ff500000, 0x00000000ff58f570, 0x00000000ff600000)

可知,此时Survivor From区中有500K对象,即那500K未知对象。所以新生代GC后会存活2M数组和500K未知对象,放不进Survivor区。但也不会让这2M数组和500K未知对象全部都进入老年代,而是会把500K的未知对象先放入Survivor From区中。

所以结合GC日志,可以清晰的看到:在这种情况下,是会把部分对象放入Survivor区的,如下图示:

图片

接着再根据如下日志:

concurrent mark-sweep generation total 10240K, used 2050K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

可以发现此时老年代里会有2M的数组,因此可以认为:YGC过后,发现存活下来的对象有2M的数组和500K的未知对象。此时会把500K的未知对象放入Survivor区,把2M的数组放入老年代。如下图示:

图片

(4)总结

这里展示了YGC后存活对象放不下S区,部分对象会进入老年代的例子。这种场景下,会有部分对象留在Survivor中,有部分对象进入老年代中。

5.JVM的Full GC日志应该怎么看

(1)示例代码

(2)GC日志

(3)分析日志

(4)总结

(1)示例代码​​​​​​​

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

(2)GC日志

接下来需要采用如下参数来运行上述程序:​​​​​​​

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

这里最关键一个参数,就是-XX:PretenureSizeThreshold=3145728。该参数设置大对象阈值为3M,即对象大小超过3M就直接进入老年代。

运行之后会得到如下GC日志:​​​​​​​

0.308: [GC (Allocation Failure) 0.308: [ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs]0.314: [CMS: 8194K->6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
 par new generation   total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 concurrent mark-sweep generation total 10240K, used 6836K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
 class space    used 300K, capacity 386K, committed 512K, reserved 1048576K

(3)分析日志

一.首先看如下代码:​​​​​​​

byte[] array1 = new byte[4 * 1024 * 1024];array1 = null;

这行代码直接分配了一个4M的大对象,此时这个对象会直接进入老年代,接着array1不再引用这个对象。如下图示:

图片

二.接着看下面的代码:​​​​​​​

byte[] array2 = new byte[2 * 1024 * 1024];byte[] array3 = new byte[2 * 1024 * 1024];byte[] array4 = new byte[2 * 1024 * 1024];byte[] array5 = new byte[128 * 1024];

连续分配了4个数组,其中3个是2M的数组,1个是128K的数组。如下图示,全部都会进入Eden区。

图片

三.接着会执行如下代码:

byte[] array6 = new byte[2 * 1024 * 1024];

此时新生代就放不下2M的对象了,因为Eden区已经不够空间了,所以会触发一次Young GC。可以参考下面的GC日志:

ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs

这行日志就显示了:Eden区原来是有7260K对象,但是回收后发现一个都回收不掉,因为上述几个数组都还被main()方法栈的局部变量引用。

所以此时就会直接把这些对象都放入到老年代里,但是现在老年代里已经有一个4M的数组了,此时老年代已经放不下3个2M的数组和1个128K的数组了。参考如下GC日志:

[CMS: 8194K->6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs]

可以清晰看到,此时执行了CMS垃圾回收器的Full GC。Full GC会对老年代进行GC,同时一般会跟一次新生代GC关联,以及触发一次元数据区(永久代)的GC。

由于在CMS的Full GC之前,就已经触发过Young GC了。所以Young GC已经有了,接着就是执行针对老年代的Old GC。也就是如下日志:

CMS: 8194K->6836K(10240K), 0.0049920 secs

这里可以看到老年代从8M左右的对象占用,变成了6M左右的对象占用。这个过程具体如下:

第一:在完成Young GC之后,先把2个2M的数组放入到老年代。如下图示:

图片

第二:如果继续往老年代放入1个2M数组和1个128K数组,则一定放不下。因此这时就会触发CMS的Full GC,然后就会回收掉老年代中的一个4M的数组,因为它已经没被引用了。如下图示:

图片

第三:接着往老年代放入1个2M的数组和1个128K的数组。如下图示:

图片

所以可以看到如下的CMS垃圾回收日志:

CMS: 8194K->6836K(10240K), 0.0049920 secs

老年代从Full GC回收前的8M变成了6M,就是上图所示。

第四:最后当CMS执行Full GC完毕后,新生代的对象都进入了老年代。此时最后一行代码要在新生代分配2M的数组就可以成功了。如下图示:

图片

(4)总结

这里介绍了一个触发老年代GC的案例:就是新生代存活对象太多,老年代都放不下了,就会触发CMS的FGC。

(5)触发老年代GC的其他场景

一.执行YGC后存活对象太多,老年代逐个放不下后会触发老年代GC

二.执行YGC前老年代可用空间小于历次YGC升入老年代对象平均大小,于是就会在执行YGC前,提前触发老年代GC

三.老年代使用率已经达到了92%的阈值,也会触发老年代GC

6.问题汇总

问题一:

JVM优化思路总结

阶段一:项目上线初期

一.上线前,根据预期的QPS、平均每个请求或者任务的内存需求大小等。评估出需要使用几台机器来承载,每台机器需要什么样的配置。

二.根据系统的请求或者任务处理速度,评估内存使用情况。然后合理分配Eden区、Survivor区、老年代的内存大小。

JVM调优的总体原则就是:

让短命对象在YGC就被回收,不要进入老年代。让长期存活的对象尽早进入老年代,不要在新生代复制来复制去。对系统响应时间敏感且内存需求大的,建议采用G1回收器。

如何合理分配各个区域:

一.根据内存增速来评估多久进行YGC

二.根据每次YGC会有多少存活对象来评估S区的大小设置是否合理

三.评估多久会进行一次FGC+产生的STW是否可接受

阶段二:项目运营出色,系统负载增加了100倍

方案1:增加服务器数量

根据系统负载的增比,同比增加机器数量,机器配置和JVM的配置可以保持不变。

方案2:使用更高配置的机器

更高的配置,意味着更快速的处理速度和更大的内存。响应时间敏感且内存需求大的使用G1回收器,这时候需要和项目上线初期一样,合理地使用配置和分配内存。

问题二:

G1存不存在类似ParNew + CMS频繁回收导致的系统变慢问题?

答:G1可能会频繁回收,但它每次回收时间可控,所以不会对系统造成太大影响。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2269507.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

javaEE-文件操作和IO-文件

目录 一.什么是文件 1.文件就是硬盘(磁盘)上的文件。 2.计算机中存储数据的设备: 3.硬盘的物理特征 4.树型结构组织和⽬录 5.文件路径 文件路径有两种表示方式: 6.文件的分类 二、java中文件系统的操作 1.File类中的属性: 2.构造方…

使用 Docker 搭建 Hadoop 集群

1.1. 启用 WSL 与虚拟机平台 1.1.1. 启用功能 启用 WSL并使用 Moba 连接-CSDN博客 1.2 安装 Docker Desktop 最新版本链接:Docker Desktop: The #1 Containerization Tool for Developers | Docker 指定版本链接:Docker Desktop release notes | Do…

数据结构(系列)

在Python中,列表(list)是一种基本的数据结构,它可以存储一系列的元素。列表是可变的,即可以对其进行增删改查操作。 栈(Stack)是一种具有特定限制的线性数据结构,在栈中&#xff0c…

【Linux】HTTP cookie与session

在登录B站时,有登录和未登录两种状态, 问题:B站是如何认识我这个登录用户的?问题:HTTP是无状态、无连接的,怎么能够记住我? HTTP协议是无状态、无连接的。比如客户端(浏览器&#…

Java - 日志体系_Simple Logging Facade for Java (SLF4J)日志门面_SLF4J集成logback 及 原理分析

文章目录 Pre官网集成步骤POM依赖使用第一步:编写 Logback 的配置文件第二步:在代码中使用 SLF4J 原理分析1. 获取对应的 ILoggerFactory2. 根据 ILoggerFactory 获取 Logger 实例3. 日志记录过程 小结 Pre Java - 日志体系_Apache Commons Logging&…

5.系统学习-PyTorch与多层感知机

PyTorch与多层感知机 前言PyTroch 简介张量(Tensor)张量创建张量的类型数据类型和 dtype 对应表张量的维度变换:张量的常用操作矩阵或张量计算 Dataset and DataLoaderPyTorch下逻辑回归与反向传播数据表格 DNN(全连结网络&#x…

ubuntu 如何使用vrf

在Ubuntu或其他Linux系统中,您使用ip命令和sysctl命令配置的网络和内核参数通常是临时的,这意味着在系统重启后这些配置会丢失。为了将这些配置持久化,您需要采取一些额外的步骤。 对于ip命令配置的网络接口和路由,您可以将这些配…

Unity Shader TexelSize的意义

TexelSize在制作玻璃折射效果时会用到。 // Get the normal in tangent space fixed3 bump UnpackNormal(tex2D(_BumpMap, i.uv.zw)); // Compute the offset in tangent space float2 offset bump.xy * _Distortion * _RefractionTex_TexelSize.xy; i.scrPos.xy offset * i…

嵌入式硬件杂谈(七)IGBT MOS管 三极管应用场景与区别

引言:在现代嵌入式硬件设计中,开关元件作为电路中的重要组成部分,起着至关重要的作用。三种主要的开关元件——IGBT(绝缘栅双极型晶体管)、MOSFET(金属氧化物半导体场效应晶体管)和三极管&#…

【亲测有效】k8s分布式集群安装部署

1.实验环境准备 准备三台centos7虚拟机,用来部署k8s集群: master(hadoop1,192.168.229.111)配置: 操作系统:centos7.3以及更高版本都可以配置:4核cpu,4G内存&#xff…

【SQL server】教材数据库(5)

使用教材数据库(1)中的数据表完成以下题目: 1 根据上面基本表的信息定义视图显示每个学生姓名、应缴书费 2 观察基本表数据变化时,视图中数据的变化。 3利用视图,查询交费最高的学生。 1、create view 学生应缴费视…

去除el-tabs 下面的灰色横线,并修改每一项的左右间距,和字体颜色

HTML <el-tabs v-model"activeName" class"demo-tabs" tab-click"handleClick"><el-tab-pane label"全部" :name"null"></el-tab-pane><el-tab-pane label"问答陪练" name"general-t…

纯血鸿蒙ArkUI的网格布局详解

网格布局概要 网格布局是由行和列分割的单元格组成&#xff0c;通过指定项目所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力&#xff0c;子组件占比控制能力&#xff0c;是一种重要的自适应布局组件&#xff0c;其使用场景有九宫格图片展示、日历、计算器等…

LeRobot(1)

Train python lerobot/scripts/train.py \ policyact \ envaloha \ env.taskAlohaInsertion-v0 \ dataset_repo_idlerobot/aloha_sim_insertion_human \ load_data一直报错&#xff0c;忘记截图了&#xff0c;反正是ssh报错&#xff0c;下不下来&#xff0c;网…

【C++】B2085 第 n 小的质数

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述题目内容输入格式输出格式题目示例 &#x1f4af;原始做法分析解法源码解析1. 步骤分析2. 问题分析3. 性能问题 &#x1f4af;老师提供的优化解法优化代码每部分解…

Node.js详细安装教程

1.下载 [名称]&#xff1a;Node.js [大小]&#xff1a;26.4 MB [语言]&#xff1a;简体中文 [安装环境]&#xff1a;Win7/Win8/Win10 [Node.js15.11.0下载链接]&#xff1a; 通过网盘分享的文件&#xff1a;Node.js 链接: https://pan.baidu.com/s/12WlNlWlX-1ppdhz…

IDEA 搭建 SpringBoot 项目之配置 Maven

目录 1?配置 Maven 1.1?打开 settings.xml 文件1.2?配置本地仓库路径1.3?配置中央仓库路径1.4?配置 JDK 版本1.5?重新下载项目依赖 2?配置 idea 2.1?在启动页打开设置2.2?配置 Java Compiler2.3?配置 File Encodings2.4?配置 Maven2.5?配置 Auto Import2.6?配置 C…

SpringCloud 系列教程:微服务的未来(三)IService接口的业务实现

本文将介绍 IService 接口的基本业务操作、复杂业务操作、Lambda 方法的使用以及批量增加操作&#xff0c;帮助开发者深入了解如何高效地利用 MyBatis-Plus 提供的功能进行数据库操作。无论是简单的单表查询&#xff0c;还是复杂的多表联动&#xff0c;甚至是大数据量的批量操作…

MAC环境安装(卸载)软件

MAC环境安装&#xff08;卸载&#xff09;软件 jdknode安装node&#xff0c;并实现不同版本的切换背景 卸载node从node官网下载pkg安装的node卸载用 homebrew 安装的node如果你感觉删的不够干净&#xff0c;可以再细分删除验证删除结果 jdk 1.下载jdk 先去官网下载自己需要的版…

VScode SSH 错误:Got bad result from install script 解決

之前vscode好好的&#xff0c;某天突然连接报错如下 尝试1. 服务器没有断开,ssh可以正常连接 2. 用管理员权限运行vscode&#xff0c;无效 3. 删除服务器上的~/.vscode-server 文件夹&#xff0c;无效 试过很多后&#xff0c;原来很可能是前一天anaconda卸载导致注册表项 步…