跳到主要内容

5. 分析GC日志

5.1 GC 日志参数

参数说明
-verbose:gc输出日志信息,默认输出的标准输出。可以独立使用
-XX:+PrintGC等同于 -verbose:gc,表示打开简化的日志。可以独立使用
-XX:+PrintGCDetails在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域的分配情况。可以独立使用
-XX:+PrintGCTimeStamps程序启动到 GC 发生的时间秒数,不可以独立使用,需要配合 -XX:+PrintGCDetails 使用。
-XX:+PrintGCDateStamps输出 GC 发生时的时间戳(以日期的形式,例如:2013-05-04T21:53:59.234+0800),不可以独立使用,可以配合 -XX:+PrintGCDetails 使用
-XX:+PrintHeapAtGC每一次 GC 前和 GC 后,都打印堆信息。可以独立使用
-Xloggc:<file>把 GC 日志写入到一个文件中去,而不是打印到标准输出中

5.2 GC 日志格式

5.2.1 复习:GC分类

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
      • 目前,只有 CMS GC 会有单独收集老年代的行为。
      • 注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
    • 目前,只有 G1 GC 会有这种行为
  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
笔记
  1. 新生代收集:当 Eden 区满的时候就会进行新生代收集,所以新生代收集和 S0 区域和 S1 区域无关
  2. 老年代收集和新生代收集的关系:进行老年代收集之前会先进行一次年轻代的垃圾收集,原因如下:一个比较大的对象无法放入新生代,那它自然会往老年代去放,如果老年代也放不下,那会先进行一次新生代的垃圾收集,之后尝试往新生代放,如果还是放不下,才会进行老年代的垃圾收集,之后在往老年代去放,这是一个过程,我来说明一下为什么需要往老年代放,但是放不下,而进行新生代垃圾收集的原因,这是因为新生代垃圾收集比老年代垃圾收集更加简单,这样做可以节省性能
  3. 进行垃圾收集的时候,堆包含新生代、老年代、元空间/永久代:可以看出 Heap 后面包含着新生代、老年代、元空间,但是我们设置堆空间大小的时候设置的只是新生代、老年代而已,元空间是分开设置的
  4. 哪些情况会触发Full GC:老年代空间不足、方法区空间不足、显示调用 System.gc()、Minior GC 进入老年代的数据的平均大小 大于 老年代的可用内存、大对象直接进入老年代,而老年代的可用空间不足

5.2.2 不同GC分类的GC细节

/**
* -XX:+PrintCommandLineFlags
*
* -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
*
* -XX:+UseParNewGC:标明新生代使用ParNew GC
*
* -XX:+UseParallelGC:表明新生代使用Parallel GC
* -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
* 说明:二者可以相互激活
*
* -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用
*/
public class GCUseTest {
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();

while(true){
byte[] arr = new byte[1024 * 10];//10kb
list.add(arr);
// try {
// Thread.sleep(5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}

java

老年代使用CMS GC

GC 设置方法:参数中使用 -XX:+UseConcMarkSweepGC,说明老年代使用 CMS GC,同时年轻代也会触发对 ParNew 的使用,因此添加该参数之后,新生代使用 ParNew GC,而老年代使用 CMS GC,整体是并发垃圾收集,主打低延迟

打印出来的GC细节:

新生代使用Serial GC

GC 设置方法:参数中使用 -XX:+UseSerialGC,说明新生代使用 Serial GC,同时老年代也会触发对 Serial Old GC 的使用,因此添加该参数之后,新生代使用 Serial GC,而老年代使用 Serial Old GC,整体是串行垃圾收集

打印出来的GC细节:

DefNew 代表新生代使用 Serial GC,然后 Tenured 代表老年代使用 Serial Old GC

5.2.3 GC日志分类

MinorGC

MinorGC(或young GC或YGC)日志:

[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
text

FullGC

[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
text

5.2.4 GC日志结构剖析

垃圾收集器

  • Serial 收集器:新生代显示 "[DefNew",即 Default New Generation
  • ParNew 收集器:新生代显示 "[ParNew",即 Parallel New Generation
  • Parallel Scavenge 收集器:新生代显示"[PSYoungGen",JDK1.7 使用的即 PSYoungGen
  • Parallel Old 收集器:老年代显示 "[ParoldGen"
  • G1 收集器:显示 "garbage-first heap"

透过日志看GC原因

  • Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据
  • Metadata GCThreshold:Metaspace 区不够用了
  • FErgonomics:JVM 自适应调整导致的 GC
  • System:调用了 System.gc() 方法

透过日志看GC前后情况

通过图示,我们可以发现GC日志格式的规律一般都是:GC前内存占用->GC后内存占用(该区域内存总大小)

[PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K)
text
  • 中括号内:GC 回收前年轻代堆大小,回收后大小,(年轻代堆总大小)
  • 括号外:GC 回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)

注意:Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系,Eden:S0:S1=8:1:1。

透过日志看GC时间

GC日志中有三个时间:user,sys和real

  • user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。
  • sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间
  • real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行 gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。

由于多核的原因,一般的 GC 事件中,real time 是小于 sys time+user time 的,因为一般是多个线程并发的去做 GC,所以 real time 是要小于 sys+user time 的。如果 real>sys+user 的话,则你的应用可能存在下列问题:IO 负载非常重或 CPU 不够用。

5.2.5 Minor GC 日志解析

2020-11-20T17:19:43.265-0800: 0.822:[GC (ALLOCATION FAILURE) [PSYOUNGGEN: 76800K->8433K(89600K)] 76800K->8449K(294400K), 0.0088371 SECS] [TIMES: USER=0.02 SYS=0.01,REAL=0.01 SECS]
text

Minor GC 日志解析

5.2.6 Full GC 日志解析

2020-11-20T17:19:43.794-0800:1.351:[FULL GC (METADATA GC THRESHOLD) [PSYOUNGGEN: 10082K->0K(89600K)] [PAROLDGEN:32K->9638K(204800K)]10114K->9638K(294400K),
[METASPACE: 20158K->20156K(1067008K)],0.0285388 SECS] [TIMES: USER=0.11SYS=0.00,REAL=0.03 SECS]
text

Minor GC 日志解析

GC 日志分析工具

5.3.1 GCEasy

GCEasy -- 一款好用的在线分析 GC 日志的网站。

官网地址:https://gceasy.io/

GCEasy是一款在线的GC日志分析器,可以通过GC日志分析进行内存泄露检测、GC暂停原因分析、JVM配置建议优化等功能,而且是可以免费使用的(有一些服务式收费的)。

5.3.2 GCViewer

上面介绍了一款在线的 GC 日志分析器,下面介绍一个离线版的 GcViewer。

GCViewer 是一个免费的、开源的分析小工具,用于可视化査看由 SUN/0racle,IBM,HP 和 BEAJava 虚拟机产生的垃圾收集器的日志。

GCViewer 用于可视化 Java VM 选项 -verbose:gc 和.NET 生成的数据 -Xloggc:<file>。它还计算与垃圾回收相关的性能指标 (吞吐量, 累积的暂停,最长的暂停等)。当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时,此功能非常有用。

安装

  1. 下载GCViewer工具
  2. 只需双击 gcviewer-1.3x.jar 或运行 java -jar gcviewer-1.3x.jar (它需要运行java1.8 vm),即可启动 GCViewer(gui)

5.3.3 其他工具

GChisto

Gchisto 是一款专业分析 GC 日志的工具,可以通过 GC 日志来分析:MinorGC、Fu11 GC 的次数、频率、持续时间等,通过列表、报表、图表等不同形式来反应 GC 的情况。

虽然界面略显粗糙,但是功能还是不错的。

注意
  • 官网上没有下载的地方,需要自己从SVN上拉下来编译
  • 不过这个工具似乎没怎么维护了,存在不少bug

HPjmeter

  • 工具很强大,但是只能打开由以下参数生成的 GC log,-verbose:gc -Xloggc:gc.log。添加其他参数生成的 gc.log 无法打开
  • HPjmeter 集成了以前的 HPjtune 功能,可以分析在 HP 机器上产生的垃圾回收日志文件