查看 jvm GC日志

[TOC]

GC日志阅读

在开发的世界里,阅读日志是最基础的能力,也是解决问题重要的工具。同样阅读gc日志也是解决虚拟机内存的基础技能,通过配置参数-XX:+PrintGCDetails就可以打印gc日志,建议加上参数-Xloggc指定gc日志目录,避免gc日志和console控制台日志混乱造成的阅读困难。
每一种收集器的日志都会略有不同,但会维持一定的共性,以下面一段日志为例:

0.332: [GC (Allocation Failure) [PSYoungGen: 6120K->504K(6144K)] 12535K->12549K(19968K), 0.0066909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.339: [Full GC (Ergonomics) [PSYoungGen: 504K->0K(6144K)] [ParOldGen: 12045K->10615K(13824K)] 12549K->10615K(19968K), [Metaspace: 3473K->3473K(1056768K)], 0.1372999 secs] [Times: user=0.28 sys=0.00, real=0.14 secs] 

最前面的0.332和0.339代表了gc的发生的时间,它的含义是表达虚拟机启动到发生gc的秒数。后面的GC和Full GC代表着垃圾收集的停顿类型,如果是GC代表的是新生代的GC,也称ygc和minor gc,fullgc代表的是对整堆的一个gc。后面括号里的Allocation Failure和Ergonomics代表的是发生gc的原因,分别是eden区域空间不够和parOldGen空间不够导致的gc和fullgc问题。以Full GC为例,接下来的[PSYoungGen、[ParOldGen、[Metaspace代表gc发生的区域,分别是年轻代、老年代、元空间,其名字也是由所使用的gc收集器密切相关,大致如下:

收集器                    显示区域
serial                   DefNew
ParNew                   ParNew
Parallel Scavenge        PSYoungGen
serial old               Tenured
parallel old             ParOldGen
CMS                      CMS

后面方括号内部的 504K->0K(6144K)代表着该区域GC前使用容量-》GC后该区域所使用容量(该区域总容量),方括号之外的12549K->10615K(19968K)则代表gc之前堆中使用容量-》gc后堆中使用容量(堆总容量)。0.1372999 secs这个很简单,代表gc占用时间,单位是秒。

内存分配与回收策略

  • 对象优先在Eden分配

大多数情况下,对象优先在新生代Eden区中分配。当Eden区域没有足够空间进行分配时,将发生一次Minor GC。虚拟机提供了-XX:+PrintGCDetails用来输出gc日志,此日志会告诉我们垃圾收集行为时的内存日志,并在进程结束后输出当前内存各区域的分配情况。上例子:

public class TestAllocation {

    private static final int _1MB=1024*1024;

    public static void main(String[] args) {
        byte[] a1,a2,a3,a4;
        a1=new byte[2*_1MB];
        a2=new byte[2*_1MB];
        a3=new byte[2*_1MB];
        a4=new byte[4*_1MB];
    }
}

gc日志如下所示:

"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetails -Xmx20m -Xms20m -Xmn10m 
[GC (Allocation Failure) [PSYoungGen: 6294K->808K(9216K)] 6294K->4912K(19456K), 0.0023349 secs] [Times: user=0.09 sys=0.00, real=0.02 secs] 
Heap
 PSYoungGen      total 9216K, used 7273K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc50670,0x00000000ffe00000)
  from space 1024K, 78% used [0x00000000ffe00000,0x00000000ffeca020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

其中vm配置参数从第一行便可知。新生代分为eden区域和两块survior区域,默认比例为8:1:1,从图中eden:from:to=8192:1024:1024可以得到验证。下来我们就根据gc日志分析下在执行这段程序时,jvm究竟都做了哪些事。

image.png

从这张图片可知,在jdk1.8.0_151中,即使跑一个空的main函数,新生代就要占2362k,这个是虚拟机的初始内存占用,好奇宝宝可以通过jmap命令看看到底堆里装了什么。现在让我们回到最开始代码中,会将a1、a2分配在新生代的eden区,此时eden区域为2048k+2048K+2362k=6458k,因为eden区域空间不够,不足以将a3装入,此时触发minor gc,又因为此时a1、a2对象还存活,suivor区域只有1024k,故将a1、a2分配担保到老年代。从日志中可知,经历过一次minor gc新生代还有808k的存活对象,因为a1、a2已经担保到老年代,故这是初始内存中经过gc存活的对象,通过复制算法转移到survivor中。此时eden区域是0k,其中一块survivor是初始内存,老年代存放着a1、a2对象,此时开始继续分配对象内存,因a3+a4<eden区域,故全部分配在eden区域。

  • 大对象直接进入老年代
    哪怕你从来没有学习过jvm知识,你或许也听说过江湖上流传着大对象直接进入老年代这个传闻。很多人都知道这个知识点,但恐怕大多数人并不能准确的去描述这个分配策略。
    1.何谓大对象?

所谓大对象就是需要大量连续内存空间的对象,上个例子中的byte数组就是典型的大对象。

2.参数-XX:PretenureSizeThreshold的作用?
虚拟机提供了-XX:PretenureSizeThreshold,令大于这个值得对象直接在老年代分配。hotspot可以在年轻代手机内存的收集器有Serial、ParNew、Parallel Scavenge以及G1(G1划分内存区域比较特殊暂不考虑)。其中只有Serial和ParNew收集器可以识别这个参数,Parallel Scavenge是不识别这个参数的,但并不是大对象直接进入老年代分配策略对其就是无效的,在Parallel Scavenge中自有它的实现,大约等于Eden区域一半的对象会被认成大对象。感兴趣的可以来这看看,传送门:链接描述.如果想要使用-XX:PretenureSizeThreshold参数,可以考虑使用ParNew+CMS的组合。给大家展示一个例子:

    public class BigObject {

    public static void main(String[] args) {
        byte[] test = new byte[4*1024*1024];
    }
}
image.png

从gc日志,我们很容易得出大对象进入老年代这个结论。对了,还需要注意的是XX:PretenureSizeThreshold的单位是k,不能像-Xmx3mb这样直接指定。
3.为什么大对象要进入老年代?
在搞清楚这个问题之前,我们首先要去揣摩大师们设计分代算法的意图。设计师们希望新生代的对象多数是朝生夕灭的,故新生代采用复制算法最合适。复制算法的优点是简单,速度快(在存活对象少的情况下),缺点是占内存要发生内存复制。这样做的目的就是避免在eden区和两个survivor区之间发生大量的内存复制。

  • 长期存活的对象将进入老年代
    虚拟机给每个对象定义了一个对象年龄计数器,保存在对象头中的Mark word部分。如果对象在eden出生,经历过一次minor gc仍然活着,并且能被survivor区容纳,将会被移动到survivor区域,并且将gc年龄设置为1,这种对象没经历一次Minor gc ,年龄就增加一岁,当它的年龄增加到一定程度(默认是15岁),就会被晋升到老年代,这个年龄阈值可以通过参数-XX:MaxTenuringThresold来设置.

  • 动态对象年龄判定
    虚拟机并不是永远要求对象的年龄永远达到MaxTenuringThresold才能晋升老年代,如果在survivor空间中相同年龄所有的对象的大小总和大于survivor空间的一半,年龄大于或等于改年龄的对象直接进入老年代,无须等到MaxTenuringThresold要求的年龄。

  • 空间分配担保
    在发生minor gc之间,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,则代表这次gc是安全的。如果不成立,jdk1.7和jdk1.8的版本中会继续检查老年代最大的可用空间是否大于历次晋升到老年代对象的平均大小,如果小于则进行一次full gc,如果大于则尝试进行一次minor gc,如果出现老年代担保失败的情况则会进行一次full gc。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容