JVM系统优化实践(13):GC动手实践

您好,我是湘王,这是我的CSDN博客,欢迎您来,欢迎您再来~


上一次留了个小尾巴:怎么以通过代码模拟对象年龄在15岁之后才进入老年代呢?自己试着实现了一下。

首先需要设置好相关的JVM环境:

-XX:InitialHeapSize=104857600 -XX:MaxHeapSize=104857600 -XX:NewSize=20971520 -XX:MaxNewSize=20971520 -XX:MaxTenuringThreshold=15 -XX:SurvivorRatio=6 -XX:PretenureSizeThreshold=10485760 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/work/logs/gc.log

然后准备示例代码:

public static void main(String[] args) {

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

for (int i = 0; i < 20; i++) {

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

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

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

array2 = null;

}

}

运行后,打印出来的GC日志如下:

JVM系统优化实践(13):GC动手实践_第1张图片

 

可以清除地看到,第15次时,还有存活对象在年轻代。而第16次时,年轻代中已没有任何存活对象。从第16次开始,存活对象始终存在于老年代,大小固定为732K。这就实现了预期的结果。

下面再来动手实现「Survivor空间不足新对象直接进入老年代」的目标。

还是老套路,准备相关JVM设置参数:

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:NewSize=10485760 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10485760 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/work/logs/gc.log

然后准备示例代码:

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];

}

代码执行前的内存分配是这样的:

JVM系统优化实践(13):GC动手实践_第2张图片

 

首先执行下面的代码:

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

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

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

执行过后,创造了3 × 2M的数组,JVM状态为:

JVM系统优化实践(13):GC动手实践_第3张图片

 

接着执行:

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

array2 = null;

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

此时eden空间不足,触发Young GC:

JVM系统优化实践(13):GC动手实践_第4张图片

 

从打印出来的GC日志可以看到:array3分配到eden区,由于S0和S1都放不下array1,它被直接分配到老年代:

JVM系统优化实践(13):GC动手实践_第5张图片

 

JVM系统优化实践(13):GC动手实践_第6张图片

 

1、Eden区使用了26%,正是array3对象;

2、Survivor From使用了60% = 625K / 1024K,保存存活对象;

3、老年代被使用了2050K,存放的是array1对象。

再来一个实例:通过代码模拟分配一个大对象,让大对象直接进入老年代。

相关JVM参数:

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:NewSize=10485760 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=1048576 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/work/logs/gc.log

示例代码:

public static void main(String[] args) {

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

array = null;

}

JVM系统优化实践(13):GC动手实践_第7张图片

 

通过GC日志可以看到:

1、Eden区有一些未知对象;

2、由于array对象大小超过-XX:PretenureSizeThreshold指定的指,所以没有触发任何GC,array被直接分配到了老年代。

继续实例:老年代空间不足导致触发GC。

相关JVM参数:

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

示例代码:

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];

}

代码执行前内存分配:

JVM系统优化实践(13):GC动手实践_第8张图片

 

执行代码:

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

array1 = null;

执行后,创造了1 × 4M的数组,JVM状态为:

JVM系统优化实践(13):GC动手实践_第9张图片

 

接着继续执行下列代码:

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];

此时的JVM内存状态:

JVM系统优化实践(13):GC动手实践_第10张图片

 

再继续执行:

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

此时eden空间不足,触发Young GC,此时的JVM内存状态:

JVM系统优化实践(13):GC动手实践_第11张图片

 

观察GC日志:

「ParNew (promotion failed): 7419K->8166K(9216K), 0.0023952 secs」

这一行说明回收失败,array2、array3、array4、array5、array6都被引用,一个都回收不掉,空间超过8M。尝试往老年代中存放,此时老年代已有一个4M大小的array1,于是老年代也放不下,触发Full GC。

「CMS: 8194K->6864K(10240K), 0.0026125 secs 11515K->6864K(19456K), [Metaspace: 2755K->2755K(1056768K)], 0.0051233 secs」

这一行说明此时执行了CMS GC,CMS: 8194K->6864K(10240K),表明老年代空间从8M变为6M。同时触发元空间的GC(Metaspace GC)

「CMS: 8194K->6864K(10240K)」

这一行说明:

1、先将array3和array4放入老年代;

2、触发CMS的Full GC,回收掉无用的数组array1;

3、将array2和arrya5再放进去;

4、因此老年代的大小就是array2 + array3 + array4 + array5 = 3 × 2M + 128K;

5、再将array6放到eden区。

Full GC的执行过程是:

JVM系统优化实践(13):GC动手实践_第12张图片

 

查看Full GC日志:

JVM系统优化实践(13):GC动手实践_第13张图片

 

可以知道:

1、Eden区使用了26%,正是array6对象;

2、老年代被使用了6864K,存放的是array2~array5对象。

再留几个小尾巴,用代码模拟出另外几种老年代GC场景:

1、触发Young GC前,老年代空间小于历次Young GC后升入老年代的对象的平均大小;

2、老年代被使用率达到92%的阈值。

一些FAQ。

关于reserved、committed、capacity、used和class space的概念澄清:

1、它们并不是纯粹的JVM概念,也和OS紧密相关;

2、reserved是操作系统为JVM进程“保留”的连续的空间,它只是记录JVM需要多少空间,并不会被提交给OS;

3、committed是当JVM进程真正要使用这个连续的空间时,OS真正分配的(也可能会分配失败);

4、capacity和used才属于JVM;

5、capacity反映了容量,used反映了实际使用量;

6、metaspace并不是将全部的空间都用来存放class对象,它还需要放所谓静态变量(比如一个Class Loader被分配了一块内存,这块内存可能并没有被用完,于是就会有产生内存碎片);

7、因此,class space是指实际上被用于放class对象的那块内存的和。


感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~

你可能感兴趣的:(技术,jvm,GC实践,G1,GC)