GC日志分析以及相关基础知识

    xiaoxiao2022-07-03  111

    前言

    最近在学习JVM的相关知识,今天来实践一下如何阅读分析GC日志。下面我们来看一个例子,创建1000个60kb大小的对象,将他们添加到list中,设置参数打印出GC,再结合jdk自带的VirsualVM来进行学习分析。我用的是idea,如果是eclipse的话也是大同小异的,方法如下:首先在运行前点击Edit Configurations-->VM options:接着对参数进行设置。接着说一下这些参数的含义。

    -verbose:gc-XX:+PrintGC-XX:+PrintGCDetails-XX:+PrintGCDateStamps-XX:+PrintGCTimeStamps

    1和2没有区别,都是输出GC的简单日志,3是打印出GC的相关细节,所以一般我们就用-XX:+PrintGCDetails,4是打印此次垃圾回收距离jvm开始运行的所耗时间,可读性较差,不利于定位问题,而5记录的系统时间,所以一般使用-XX:+PrintGCTimeStamps。

     

    参数:

    -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps  package tree; /** * Created by Hollake on 2019\5\21 0021. */ import java.util.ArrayList; import java.util.List; public class JvmTest { public byte[] placeholder = new byte[60*1024]; public static void main(String[] args) throws Exception { fileHeap(1000); // System.gc(); } public static void fileHeap(int num) throws InterruptedException{ List<JvmTest> list = new ArrayList<>(); for (int i = 0; i < num; i++) { // 方便观察 Thread.sleep(100); list.add(new JvmTest()); } System.gc(); } }

    输出结果:

    2019-05-22T22:30:51.583+0800: [GC (Allocation Failure) [PSYoungGen: 25581K->4072K(29696K)] 25581K->14827K(98304K), 0.0148188 secs] [Times: user=0.05 sys=0.02, real=0.02 secs] 2019-05-22T22:31:26.504+0800: [GC (Allocation Failure) [PSYoungGen: 29672K->4080K(29696K)] 40427K->35748K(98304K), 0.0218075 secs] [Times: user=0.03 sys=0.03, real=0.02 secs] 2019-05-22T22:32:01.228+0800: [GC (Allocation Failure) [PSYoungGen: 29647K->4080K(29696K)] 61315K->56646K(98304K), 0.0205083 secs] [Times: user=0.01 sys=0.05, real=0.02 secs] 2019-05-22T22:32:01.248+0800: [Full GC (Ergonomics) [PSYoungGen: 4080K->0K(29696K)] [ParOldGen: 52565K->56543K(68608K)] 56646K->56543K(98304K), [Metaspace: 9296K->9296K(1058816K)], 0.0508283 secs] [Times: user=0.13 sys=0.00, real=0.05 secs] Heap PSYoungGen total 29696K, used 7152K [0x00000000fdf00000, 0x0000000100000000, 0x0000000100000000) eden space 25600K, 27% used [0x00000000fdf00000,0x00000000fe5fc058,0x00000000ff800000) from space 4096K, 0% used [0x00000000ff800000,0x00000000ff800000,0x00000000ffc00000) to space 4096K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x0000000100000000) ParOldGen total 68608K, used 56543K [0x00000000f9c00000, 0x00000000fdf00000, 0x00000000fdf00000) object space 68608K, 82% used [0x00000000f9c00000,0x00000000fd337fd8,0x00000000fdf00000) Metaspace used 9311K, capacity 9654K, committed 9984K, reserved 1058816K class space used 1076K, capacity 1159K, committed 1280K, reserved 1048576K

    堆和Metaspace区域的分布图

     

    可以看出主要分为5个区域,分别为Eden、Survivor0、Survivor1、Old Gen、Metaspace五个区域,其中前三个是年轻代,而Old Gen为老年代,Metaspace是原数据区域,jdk1.8之前为永生代(Perm Gen)。

    Eden,大多数情况下对象优先分配在Eden区域,在Eden区域空间不足时JVM会发起一次Minor GC。初生代内存区域大小可以通过-Xmn来调节,例如代码中设置的参数为-Xms100m,-Xmx100m,意思是堆的最小和最大内存为100Mb,而-Xmn没有设置。Survivor分为两个区域,分别为Survivor From和Survivor To两个区域,它们与Eden区域内存大小的默认比例为8:1:1,但是实际中如果不设置-XX:SurvivorRatio输出结果不一定是8:1:1,这个值可以通过-XX:SurvivorRatio来调节,每次都只会使用Eden区域和其中一个Survivor区域,在每次Minor GC发生的时候,Eden区域存活的对象存活的对象会一次性移动到空闲的另外一个Survivor区域,不能存活的会被GC,而已经使用的Survivor区域中的对象达到可以移动到老年代条件的移动到老年代,没有达到条件的移动到空闲的那块Survivor区域,如此循环往复,刚生成的Eden中新对象移动到To,From移动到To,From清空,下次就是Eden中新对象移动到From,To移动到From,To清空。老年代:大对象或者长期存活的对象进入老年代,典型的大对象就是很长的字符串或者数组,它们在被创建后会直接进入老年代;那么JVM如何知道哪些对象可以进入老年代,哪些不可以呢,为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置;另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代。而当这个区域快要满的时候会进行Full GC。Metaspace:这个区域主要存放一些虚拟机加载的类信息,这个区域在jdk8之前是永生代(Perm Gen),从JDK8开始,永久代(PermGen)的概念被废弃掉了,Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关,在达到默认阈值后,这个区域也会引发Full GC。在只有指定堆容量的情况下,新生代和老年代的比例参数由-XX:NewRatio来设定,默认为2.也就是100MB,初生代占有

    GC输出信息分析

    我们一行一行的来阅读分析:

    2019-05-22T22:30:51.583+0800: [GC (Allocation Failure) [PSYoungGen: 25581K->4072K(29696K)] 25581K->14827K(98304K), 0.0148188 secs] [Times: user=0.05 sys=0.02, real=0.02 secs] 

    2019-05-22T22:30:51.583+0800:GC发生的系统时间。GC:GC前面没有Full,表明这是Minor GC。Allocation Failure:GC发生的原因,由于年轻代中没有空间来存放新的数据结构引起的。PSYoungGen:垃圾回收器的名称,这里采用Parallel Scavenge年轻代回收器。25581K->4072K(29696K):25581K表示当前内存区域(也就是新生代中的Eden和一个Survivor区域)GC前的内存大小,4072K表示回收后的内存大小,29696K表示当前内存区域的最大可用内存。25581K->14827K(98304K):25581K表示堆的总内存当前大小,14827K表示GC后的堆的内存大小,98304K表示堆的最大可用内存,这里显示的堆内存大小稍微小于102400K,我有点疑惑。0.0148188 secs:GC事件持续的时间,以秒为单位。[Times: user=0.05 sys=0.02, real=0.02 secs] : user – 在此次垃圾回收过程中, 所有 GC线程所消耗的CPU时间之和。sys – GC过程中中操作系统调用和系统等待事件所消耗的时间。real – 应用程序暂停的时间。

    接下里的2次minor GC类似就不做介绍了,接着我们看最后一次的Full GC。

    2019-05-22T22:32:01.248+0800: [Full GC (Ergonomics) [PSYoungGen: 4080K->0K(29696K)] [ParOldGen: 52565K->56543K(68608K)] 56646K->56543K(98304K), [Metaspace: 9296K->9296K(1058816K)], 0.0508283 secs] [Times: user=0.13 sys=0.00, real=0.05 secs] 

    2019-05-22T22:32:01.248+0800:GC发生的系统时间。Full GC:表明这是Full GC。Allocation Failure:Full GC发生的原因,Ergonomics 表示JVM内部环境认为此时可以进行一次垃圾收集。PSYoungGen:垃圾回收器的名称,这里采用Parallel Scavenge年轻代回收器。4080K->0K(29696K):4080K表示当前内存区域(也就是新生代中的Eden和一个Survivor区域)GC前的内存大小,0K表示回收后的内存大小,29696K表示当前内存区域(Eden和一个Survivor)的最大可用内存。ParOldGen:垃圾回收器的名称,这里采用Parallel Old老年代回收器。52565K->56543K(68608K):52565K表示老年代Full GC前的内存大小,56543K是Full GC后的内存大小,68608K是老年代内存区域的最大值。56646K->56543K(98304K):56646K表示当前堆GC前的总内存大小,56543K表示Full GC后的内存大小,98304K是老年代内存区域的最大值。[Metaspace: 9296K->9296K(1058816K)], 0.0508283 secs]:表示Metaspace区域在GC前后大小一直为9296K,也就是没有进行GC,所有数据都保留,当前区域的最大可用内存为1058816K。注意,在创建大量类的时候,Full GC也有可能会是这个区域引起。在前面也提到过,每次minor GC发生的时候在初生代都是用到Eden区域和一个Servivor区域,默认比例是8:1:1,我们设置的堆内存大小为100MB,默认的初生代和老年代内存比例为1:2,那么就是初生代占有100MB的三分之一,也就是大致为34133K。[Times: user=0.13 sys=0.00, real=0.05 secs] : user – 在此次垃圾回收过程中, 所有 GC线程所消耗的CPU时间之和。sys – GC过程中中操作系统调用和系统等待事件所消耗的时间。real – 应用程序暂停的时间。

    最后我们看一下输出的heap的总信息。

    Heap PSYoungGen total 29696K, used 7152K [0x00000000fdf00000, 0x0000000100000000, 0x0000000100000000) eden space 25600K, 27% used [0x00000000fdf00000,0x00000000fe5fc058,0x00000000ff800000) from space 4096K, 0% used [0x00000000ff800000,0x00000000ff800000,0x00000000ffc00000) to space 4096K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x0000000100000000) ParOldGen total 68608K, used 56543K [0x00000000f9c00000, 0x00000000fdf00000, 0x00000000fdf00000) object space 68608K, 82% used [0x00000000f9c00000,0x00000000fd337fd8,0x00000000fdf00000) Metaspace used 9311K, capacity 9654K, committed 9984K, reserved 1058816K class space used 1076K, capacity 1159K, committed 1280K, reserved 1048576K 我们先按照我们设定的堆容量参数100MB来计算一下是否吻合。设置heap的容量固定到100MB=102400K。输出信息显示:Eden space :25600K,程序运行结束,27%已使用,from和to都为4096K,三者加起来为33792K,ParOldGen显示老年代总容量为68608K,所以新生代+老年代就是设置的heap容量,33792K+68608K=102400K,这符合预期,68608/33792=2.03符合默认参数的新生代和老年代的比例为1:2。但是为什么在新生代中Eden和From和To的比例不是8:1:1呢,而是6.25:1:1呢,这个结果的前提是没有设置SurvivorRatio参数,如果设置为默认值8,那么结果为8:1:1,我查看了一些文章,可能是因为JVM在程序运行过程中将参数进行了自动调整,如果不是这个原因,希望可以说明一下真正原因。至于Metaspace 使用了9311K,而容量为9654K,reserved为1048576K,这个capacity会动态扩容,关于Metaspace的信息可以看一下这篇文博客jdk8 Metaspace 调优

    最后我们看一下程序运行完后jdk自带的VisualVM所展示的结果:

    程序设定生成了60MB的对象,如果生成的对象大于老年代的最大容量,那么在最后进行多次Full GC无果后会抛出OOM异常。下面我们再次进行实验。

    再次实验

    我们将代码中的64改为100,每个对象约为10k,那么循环1000次就是10000K,除去两个Survivor区,heap内存会小于100MB,所以应该会抛出OOM异常。

    public byte[] placeholder = new byte[100*1024];

    实验结果:

    2019-05-23T15:50:34.951+0800: [GC (Allocation Failure) [PSYoungGen: 27646K->3052K(30720K)] 27646K->21863K(99328K), 0.0233884 secs] [Times: user=0.02 sys=0.05, real=0.02 secs] 2019-05-23T15:50:37.401+0800: [GC (Allocation Failure) [PSYoungGen: 30669K->3052K(30720K)] 49480K->46478K(99328K), 0.0244951 secs] [Times: user=0.02 sys=0.05, real=0.02 secs] 2019-05-23T15:50:37.425+0800: [Full GC (Ergonomics) [PSYoungGen: 3052K->0K(30720K)] [ParOldGen: 43426K->46401K(68608K)] 46478K->46401K(99328K), [Metaspace: 8811K->8811K(1056768K)], 0.0435525 secs] [Times: user=0.16 sys=0.01, real=0.04 secs] 2019-05-23T15:50:40.146+0800: [Full GC (Ergonomics) [PSYoungGen: 27595K->5000K(30720K)] [ParOldGen: 46401K->68122K(68608K)] 73997K->73122K(99328K), [Metaspace: 8930K->8930K(1056768K)], 0.0663073 secs] [Times: user=0.11 sys=0.02, real=0.07 secs] 2019-05-23T15:50:42.413+0800: [Full GC (Ergonomics) [PSYoungGen: 27571K->27013K(30720K)] [ParOldGen: 68122K->68118K(68608K)] 95693K->95132K(99328K), [Metaspace: 8942K->8942K(1056768K)], 0.0461335 secs] [Times: user=0.14 sys=0.00, real=0.05 secs] 2019-05-23T15:50:42.509+0800: [Full GC (Ergonomics) [PSYoungGen: 27592K->26913K(30720K)] [ParOldGen: 68118K->68383K(68608K)] 95711K->95297K(99328K), [Metaspace: 8942K->8887K(1056768K)], 0.0788523 secs] [Times: user=0.25 sys=0.00, real=0.08 secs] 2019-05-23T15:50:42.638+0800: [Full GC (Ergonomics) [PSYoungGen: 27642K->27414K(30720K)] [ParOldGen: 68383K->68383K(68608K)] 96026K->95797K(99328K), [Metaspace: 8895K->8895K(1056768K)], 0.0243008 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 2019-05-23T15:50:42.672+0800: [Full GC (Ergonomics) [PSYoungGen: 27573K->27515K(30720K)] [ParOldGen: 68383K->68383K(68608K)] 95956K->95898K(99328K), [Metaspace: 8906K->8906K(1056768K)], 0.0255330 secs] [Times: user=0.06 sys=0.00, real=0.03 secs] 2019-05-23T15:50:42.708+0800: [Full GC (Ergonomics) [PSYoungGen: 27615K->27615K(30720K)] [ParOldGen: 68383K->68383K(68608K)] 95998K->95998K(99328K), [Metaspace: 8906K->8906K(1056768K)], 0.0132191 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 2019-05-23T15:50:42.741+0800: [Full GC (Ergonomics) [PSYoungGen: 27615K->27615K(30720K)] [ParOldGen: 68583K->68583K(68608K)] 96198K->96198K(99328K), [Metaspace: 8906K->8906K(1056768K)], 0.0129819 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 2019-05-23T15:50:42.754+0800: [Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at tree.JvmTest.<init>(JvmTest.java:9) at tree.JvmTest.fileHeap(JvmTest.java:20) at tree.JvmTest.main(JvmTest.java:12) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

    可以看出JVM最后进行多次Full GC无果后抛出了 java.lang.OutOfMemoryError: Java heap space,从VisualVM也可以看出

    总结分析

    可以看出Eden区域大小呈现线性增加,在容量不够容纳新对象的时候会进行Minor GC,将对象从Eden和其中一个Survivor复制到另外一个Survivor,而在Full GC时,一定也会进行Minor GC,除了Minor应该有的动作外,Survivor中的对象还会经过JVM的判断,将认为可以移动到老年代中的对象移动到老年代。

    Full GC何时进行?

    现在我们知道,在新生代内存不足,无法存放要生成的对象时,Minor GC会进行,但是Full GC何时进行呢?可以看参考这篇文章https://blog.csdn.net/Hollake/article/details/90484027

     

    相关文献:

    jstat命令查看jvm的GC情况 (以Linux为例)

    jdk8 Metaspace 调优

    参考文献:

    https://blog.csdn.net/renfufei/article/details/54885190

    《深入理解Java虚拟机》

    最新回复(0)