手把手教你如何写出完美的JVM的Young GC

Posted JavaEdge.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你如何写出完美的JVM的Young GC相关的知识,希望对你有一定的参考价值。

模拟JVM的Young GC

JVM参数示范(基于JDK 1.8)

用如下JVM参数运行代码:

# 初始新生代大小 5M
-XX:NewSize=5242880
# 最大新生代大小 5M
-XX:MaxNewSize=5242880
# 初始堆大小 10M
-XX:InitialHeapSize=10485760
# 最大堆大小 10M
-XX:MaxHeapSize=10485760
-XX:SurvivorRatio=8
# 大对象阈值是10MB
-XX:PretenureSizeThreshold=10485760
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

如何打印JVM GC日志?

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

实例

对象是如何分配在Eden

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

该行会在JVM Eden内放入一个1M数组,同时在main线程的虚拟机栈压入一个main方法栈帧,其栈帧内部有一“arr1”变量,该变量指向Eden的那1M数组:

arr1 = new byte[1024 * 1024];

此时会在Eden创建第二个数组,局部变量指向其。然后第一个数组无人引用,成了垃圾:

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

在Eden创建第三个数组,同时让arr1指向第三个数组,此时前两个数组都无人引用,都成了垃圾:

arr1 = null;

arr1啥都不指了,导致之前创建的3数组全部变成垃圾:

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

分配一个2MB大小的数组,尝试放入Eden,这时Eden放的下吗?

显然不行,Eden共4M,已放入3个1M数组,只剩1M,所以这时就会触发Y-GC。

采用指定JVM参数运行程序

然后运行即可,运行完后,会出现gc.log文件,即本次程序运行的gc日志:

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

这次运行程序的JVM参数,包含:

  • 我们自己手动设置的

  • 默认的参数设置

    给JVM加个打印gc日志的参数,就可以在此看到JVM默认参数配置

一次GC的路途

0.410: [GC (Allocation Failure) 0.410: [ParNew: 3863K->465K(4608K), 0.0025689 secs] 3863K->1491K(9728K), 0.0029347 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

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执行情况:

  • GC (Allocation Failure),看字面意思即可,为啥会发生一次GC呢?

    因为要分配一个2M数组,但Eden内存不足,所以“Allocation Failure”,内存分配失败,所以就要触发一次Y-GC。

那这次GC何时发生?

看一个数字,“0.410”:你的系统运行后,过了多少s发生的本次GC,此处就是系统运行后的410ms发生了GC。

ParNew: 3863K->465K(4608K), 0.0025689 secs

此处触发的是Y-GC,所以用的也就是指定的ParNew来执行GC。

3863K->465K(4608K)

新生代可用空间4608KB,约4.5MB,Eden是4M,两个Survivor只有一个可放存活对象,另外一个必须保持空闲,所以年轻代可用空间是 Eden+1个Survivor=4.5MB。

3863K->465K:对年轻代执行一次GC,GC前使用了3863K,但GC后只有465K对象存活。

GC前,我们在Eden只放了3个1M数组,共3MB,即3072K,那为啥这里显示使用了3863K?

  • 自定义创建的数组本身虽是1MB,但为存储该数组,JVM内置还会附带一些其它信息,所以每个数组实际占用的内存是大于1MB的
  • 可能还有一些你看不见的对象在Eden里

所以GC前,三个数组+其他未知对象=3863K内存。GC前,我们在Eden只放了3个1M数组,共3MB,即3072K,那为啥这里显示使用了3863K?

  • 自定义创建的数组本身虽是1MB,但为存储该数组,JVM内置还会附带一些其它信息,所以每个数组实际占用的内存是大于1MB的
  • 可能还有一些你看不见的对象在Eden里

所以GC前,三个数组+其他未知对象=3863K内存。

0.0025689 secs:本次GC耗费时间,看这里来说大概耗费2.5ms,仅回收3MB对象而已。

3863K->1491K(9728K), 0.0029347 secs

整个Java堆内存的情况。整个Java堆内存总可用空间9728K(9.5M)=年轻代4.5MB+老年代5M。

  • GC前,整个Java堆内存使用了3863K
  • GC后Java堆内存使用了1491K

[Times: user=0.01 sys=0.00, real=0.01 secs]

本次gc消耗的时间,这里最小单位是小数点之后两位,单位是s。

图解GC执行过程

ParNew执行GC,回收掉自定义创建的三个数组,此时因为他们都无人引用,必成垃圾:

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

gc回收后,从4030K内存使用降低到512K内存使用。即这次gc日志有512KB的对象存活,从Eden区转移到Survivor1区:

GC后的堆内存

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

JVM退出时打印的当前堆内存使用情况

ParNew负责的新生代共4608K(4.5MB)可用内存,目前使用2630K(2.5MB)。此时在JVM退出前,为何新生代占了2.5M?GC后,通过如下代码又分配了个2M数组:

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

所以此时在Eden一定会有个 2M数组=2048K,然后上次GC后,在From Survivor存活了个512K未知对象,那么:

2048 K B + 512 K B = 2560 K B 2048KB + 512KB = 2560KB 2048KB+512KB=2560KB

每个数组会额外占据一些内存存放一些自己这个对象的元数据,可认为多出来的70K是数组对象额外使用的内存空间。

Eden日志

Eden此时4M内存被用52%,就因为那2M数组。然后From Survivor区,512K是100%使用率,此时被之前GC后存活下来的512K未知对象占据。

CMS、Metaspace元数据空间和Class空间

Concurrent Mark-Sweep垃圾回收器,即CMS管理的老年代内存空间共5M,此时使用了62K。
Metaspace元数据空间和Class空间,存放一些类信息、常量池之类的东西,此时他们的总容量,使用内存等。

以上是关于手把手教你如何写出完美的JVM的Young GC的主要内容,如果未能解决你的问题,请参考以下文章

JVM:28 面试题:解释一下什么是Young GC和Full GC?

JVM:30 实验:模拟频繁Young GC的场景

JVM:29 面试题:Young GC和Full GC分别在什么情况下会发生?

JVM GC杂谈之理论入门

72.JVM参数设置(各种jvm参数)分析;Total Heap;The Young Generation;

简单谈谈JVM中的GC(上)