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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 模拟JVM的Young GCJVM参数示范(基于JDK 1.8)

模拟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


17.png


如何打印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


实例

16.png


对象是如何分配在Eden

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

1

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

15.png



arr1 = new byte[1024 * 1024];


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

14.png



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


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


13.png


arr1 = null;


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




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


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


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

采用指定JVM参数运行程序

12.png


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

11.png



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


10.png9.png




这次运行程序的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,回收掉自定义创建的三个数组,此时因为他们都无人引用,必成垃圾:

8.png



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


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

7.png



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 = 2560KB2048KB+512KB=2560KB


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

6.png


Eden日志

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


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

Concurrent Mark-Sweep垃圾回收器,即CMS管理的老年代内存空间共5M,此时使用了62K。

Metaspace元数据空间和Class空间,存放一些类信息、常量池之类的东西,此时他们的总容量,使用内存等。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3月前
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)
|
8月前
|
Arthas 监控 Java
(十一)JVM成神路之性能调优篇:GC调优、Arthas工具详解及各场景下线上最佳配置推荐
“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。
684 3
|
5月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
215 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
4月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
5月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
257 3
|
5月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
5月前
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
112 0
|
8月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
158 8
|
7月前
|
算法 Java 应用服务中间件
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
|
7月前
|
存储 监控 算法
深入解析JVM内部结构及GC机制的实战应用
深入解析JVM内部结构及GC机制的实战应用