Bootstrap

JVM小册(1)------jstat和Parallel GC日志

JVM小册(1)------jstat和Parallel GC日志

一. 背景

在生产环境中,有时候会遇到OOM的情况,抛开Arthas 等比较成熟的工具以外,我们可以使用java 提供的jatatjpsjmap等工具来帮助我们排查问题和定位原因,本系列小册先简单记录一下实际使用方式,后续对JVM进行分析.

二. 场景举例

比如目前有一台服务器,部署了一个java服务,我们为它分配的堆内存如下:

#最大堆内存4G,最小2G, 一般来说建议这2个参数设置成一样的,减少因为GC以后动态内存扩容对系统造成的开销
-Xmx4096M -Xms2048M

那么怎么来查看这个服务在运行过程中的GC情况和JVM中内存的使用情况呢?

1. 添加JVM参数

首先我们要在jar包启动的时候添加打印GC日志 的参数,这里参数如下:

#打印GC详细信息
-XX:+PrintGCDetails
#打印GC时间
-XX:+PrintGCTimeStamps
#输出GC日志
-Xloggc:gc.log

添加完以上三个参数以后,在jar启动后,会在你-Xloggc 参数指定位置生成对应的gc日志,如下:

image-20210426112024708

日志的内容我们等下再详细讲解.接下来比如我们以上配置的java服务发送了OOM,或者你监控发现该服务的内存占用持续走高, 这个时候我们可能会去排查原因.

2. 使用jps 命令查看java进程的PID

在该服务部署的服务器上,运行命令jps,你可能会得到以下信息:

6 xxx.jar
366 Jps

上面的返回结果中,第一行表示当前运行的一个java进程,前面的数字是该进程的PID,下面一行是Jps命令本身,不用管.

3. 使用jstat命令查看JVM内存活动

根据第二步查到的该服务的PID是6, 我们就可以用jstat命令去查看内存活动,命令如下:

/opt/app # jstat -gc 6 1000 100

命令解释: 间隔1秒打印100次PID为6的java进程的gc情况

打印结果如下:

image-20210426113609789

其实jstat命令除了-gc参数外,还有很多可选,这里先不做扩展.现在我们得到的结果要怎么看呢?

4. jstat结果解释

以上的图片中,每一个指标的解释如下:

 S0C       From Survivor区大小(字节)
 S1C       To Survivor区大小(字节)
 S0U       From Survivor区当前使用的内存大小(字节)
 S1U       To Survivor区当前使用的内存大小(字节)
 EC        Eden区的大小(字节)
 EU        Eden区当前使用大小(字节)
 OC        老年代大小(字节)
 OU        老年代当前使用大小(字节)
 MC        方法区(永久代,元数据区)大小(字节)
 MU        方法区当前使用的大小(字节)
 CCSC      当前压缩类空间的容量
 CCSU      当前压缩类空间也是用的容量
 YGC       系统运行迄今为止的YoungGC次数
 YGCT      YoungGC耗时
 FGC       系统运行迄今为止的Full GC次数
 FGCT      FullGC耗时
 GCT       所有GC总耗时
5. 根据GC情况分析

根据jstat查看出来的gc情况,我们可能需要得到以下几个指标:

新生代对象增长的速率:
YoungGC的触发频率:
YoungGC的耗时:
每次YoungGC后有多少对象存活下来:
每次YoingGC后有多少对象进入了老年代:
老年代对象增长的速率:
FullGC触发频率:
FullGC的耗时:

根据以上参数,再加上对JVM内存模型的认识,我们可以清楚的知道当前jvm进程的运行情况,有助于我们对问题的定位.

6. GC日志分析
  • 对于已经启动的jvm进程,我们可以使用如下命令查看当前jvm使用的参数,版本,以及垃圾回收器等信息:

    java -XX:+PrintCommandLineFlags -version
    
  • 也可以使用如下命令查看当前jvmGC的详细信息

    java -XX:+PrintGCDetails -version
    

1) Parallel Scavenge + Serial Old GC日志分析

接着回过头来说gc日志,我们之前在jar启动参数中加入了打印gc信息的参数.在jvm启动以后,会在我们指定的目录打印gc日志,我们打开刚才那个应用的gc日志,你可能会看到以下信息:

OpenJDK 64-Bit Server VM (25.212-b04) for linux-amd64 JRE (1.8.0_212-b04), built on May  4 2019 17:56:12 by "buildozer" with gcc 8.3.0
Memory: 4k page, physical 5120000k(5118348k free), swap 0k(0k free)

CommandLine flags: -XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=4294967296 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 

4.737: [GC (Metadata GC Threshold) [PSYoungGen: 367429K->23240K(611840K)] 367429K->23256K(2010112K), 0.0300742 secs] [Times: user=0.04 sys=0.01, real=0.03 secs] 

4.768: [Full GC (Metadata GC Threshold) [PSYoungGen: 23240K->0K(611840K)] [ParOldGen: 16K->22304K(1398272K)] 23256K->22304K(2010112K), [Metaspace: 20775K->20775K(1067008K)], 0.0600170 secs] [Times: user=0.09 sys=0.00, real=0.06 secs] 

6.329: [GC (Allocation Failure) [PSYoungGen: 524800K->8127K(611840K)] 547104K->30439K(2010112K), 0.0197683 secs] [Times: user=0.04 sys=0.00, real=0.02 secs] 

7.776: [GC (Allocation Failure) [PSYoungGen: 532927K->18132K(611840K)] 555239K->40444K(2010112K), 0.0574908 secs] [Times: user=0.04 sys=0.00, real=0.06 secs] 

8.528: [GC (Allocation Failure) [PSYoungGen: 542932K->16422K(928256K)] 565244K->38742K(2326528K), 0.0376001 secs] [Times: user=0.09 sys=0.00, real=0.03 secs] 

8.755: [GC (GCLocker Initiated GC) [PSYoungGen: 184691K->9923K(928256K)] 207011K->32243K(2326528K), 0.0178837 secs] [Times: user=0.04 sys=0.01, real=0.02 secs] 

8.859: [GC (Metadata GC Threshold) [PSYoungGen: 91421K->8481K(1247232K)] 113741K->30801K(2645504K), 0.0144768 secs] [Times: user=0.03 sys=0.01, real=0.01 secs]

8.873: [Full GC (Metadata GC Threshold) [PSYoungGen: 8481K->0K(1247232K)] [ParOldGen: 22320K->16585K(1398272K)] 30801K->16585K(2645504K), [Metaspace: 34975K->34975K(1081344K)], 0.1082484 secs] [Times: user=0.20 sys=0.01, real=0.11 secs] 

10.761: [GC (Allocation Failure) [PSYoungGen: 1223168K->15163K(1247744K)] 1239753K->31748K(2646016K), 0.0446415 secs] [Times: user=0.05 sys=0.02, real=0.04 secs]

13.097: [GC (Allocation Failure) [PSYoungGen: 1238331K->23551K(1343488K)] 1254916K->68603K(2741760K), 0.1209356 secs] [Times: user=0.14 sys=0.02, real=0.13 secs]

15.171: [GC (Allocation Failure) [PSYoungGen: 1343487K->38905K(1358848K)] 1388539K->99859K(2757120K), 0.1189547 secs] [Times: user=0.15 sys=0.02, real=0.12 secs] 

17.227: [GC (Allocation Failure) [PSYoungGen: 1358841K->23895K(1339392K)] 1419795K->104346K(2737664K), 0.0751066 secs] [Times: user=0.11 sys=0.01, real=0.07 secs] 

18.043: [GC (Metadata GC Threshold) [PSYoungGen: 414691K->7705K(1349120K)] 495143K->100326K(2747392K), 0.0331763 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] 

18.077: [Full GC (Metadata GC Threshold) [PSYoungGen: 7705K->0K(1349120K)] [ParOldGen: 92620K->56466K(1398272K)] 100326K->56466K(2747392K), [Metaspace: 57545K->57545K(1101824K)], 0.2955924 secs] [Times: user=0.52 sys=0.01, real=0.30 secs] 

21.465: [GC (Allocation Failure) [PSYoungGen: 1300480K->27367K(1348096K)] 1356946K->83834K(2746368K), 0.0575351 secs] [Times: user=0.09 sys=0.00, real=0.06 secs] 

30.452: [Full GC (Metadata GC Threshold) [PSYoungGen: 15506K->0K(1351168K)] [ParOldGen: 90168K->76729K(1721856K)] 105674K->76729K(3073024K), [Metaspace: 95611K->95607K(1136640K)], 0.3909874 secs] [Times: user=0.69 sys=0.01, real=0.39 secs] 

94.216: [GC (Allocation Failure) [PSYoungGen: 1307648K->23645K(1350656K)] 1384377K->100382K(3072512K), 0.0587363 secs] [Times: user=0.14 sys=0.00, real=0.06 secs] 

对于以上日志,我们应该怎么分析查看呢?

这里我们为了排除干扰,我手动将日志进行了换行, 并且,一些同类型的日志我都删掉了,这样做的原因是我们先一起来看下这个日志的构成以及如何查看.知道如何查看以后,可以再根据日志的时间线来分析日志.

那么上面的日志我简化成如下部分:

image-20210426141954740

可以看到,我简化后,将日志划分成了14句,下面我们依次来看:

1

OpenJDK 64-Bit Server VM (25.212-b04) for linux-amd64 JRE (1.8.0_212-b04), built on May  4 2019 17:56:12 by "buildozer" with gcc 8.3.0
Memory: 4k page, physical 5120000k(5118348k free), swap 0k(0k free)

这一部分主要是表明当前java虚拟机的版本、JDK的版本还有服务器的系统内核等参数.

2

CommandLine flags: -XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=4294967296 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC

这一句日志中表明当前JVM采用的参数是哪些.如果不设置jvm参数, 也可以通过这部分日志来得出当前jvm默认的参数.可以看到当前我们使用的垃圾回收器是Parallel, 这也是1.8默认的垃圾回收器.

3

4.737: [GC (Metadata GC Threshold) [PSYoungGen: 367429K->23240K(611840K)] 367429K->23256K(2010112K), 0.0300742 secs] [Times: user=0.04 sys=0.01, real=0.03 secs] 
  • 4.737:

    表示GC发生的时间,这里显示的是秒级别的时间,如果你再jvm启动的时候加入了参数-XX:+PrintGCDateStamps它也会打印出当前GC的具体日期.

  • [GC (Metadata GC Threshold)

    这里表示当前发生GC的原因,在这里是由于Metadata区导致的GC. 但是在这里我很疑惑,1.8中,jvm中的永久代便用Metadata元空间来取代了,而元空间的内存使用的是堆外内存,既然如此,为什么元空间会不足呢?

    查阅资料后发现,在默认情况下,元空间大小的默认值是21810376B,也就是大约20M左右.

    查看当前jvm初始化参数命令: java -XX:+PrintFlagsInitial

    如果要自定义元空间的大小,可以使用参数-XX:MetaspaceSize=128M来设置.

  • [PSYoungGen: 367429K->23240K(611840K)]

    这一句的意思是,Young去发生了GC,在GC前Young区是367429k,在GC后是23240k,(611840k)标识Yuong区总大小.

  • 367429K->23256K(2010112K)

    这句的意思是,堆内存在GC前的大小-->GC后的大小(堆内存总大小)

  • 0.0300742 secs

    这里表示本次GC的持续时间是0.0300742秒

  • [Times: user=0.04 sys=0.01, real=0.03 secs]

    这里是本次GC的详细的时间信息

    1. real: 指的是在此次GC中所花费的总时间
    2. user: 指的是CPU工作在用户态所花费的时间
    3. sys: 指的是CPU工作在内核态所花费的时间

    详细的解释可以参考StackOverflow文章

    在这里,user+sys是CPU花费的实际时间,这个值统计了所有CPU上的时间,如果进程在多线程的环境下,这个值是会超出real的值的,也就是user+sys>=real

    存在多线程的原因在于,在回收过程中,存在并发的GC算法,比如ParNewParallelOld.

4

4.768: [Full GC (Metadata GC Threshold) [PSYoungGen: 23240K->0K(611840K)] [ParOldGen: 16K->22304K(1398272K)] 23256K->22304K(2010112K), [Metaspace: 20775K->20775K(1067008K)], 0.0600170 secs] [Times: user=0.09 sys=0.00, real=0.06 secs] 

这部分日志中,我们只对上面没有提及的部分做说明.

  • Full GC (Metadata GC Threshold)

    表示当前发送的是Full GC

  • [ParOldGen: 16K->22304K(1398272K)]

    表示老年代Old区发送的垃圾回收,回收前的大小 -> 回收后的大小(Old区总大小)

  • [Metaspace: 20775K->20775K(1067008K)]

    表示元空间区域发送的垃圾回收, 回收前是20775k,回收后是20775k,总大小是1067008k

基本上,到这里的话,Parallel的GC日志我们已经分析完了.下面开始CMS和ParNew垃圾回收器产生的GC日志分析


2) ParNew + CMS GC日志分析

JDK1.8默认使用的垃圾回收器是Parallel,该垃圾回收在进行GC时是单线程的,因此建议在生产环境中使用ParNew+CMS垃圾回收器.

-XX:+UseConcMarkSweepGC

使用以上JVM参数即可指定jvm的垃圾回收器为: 新生代ParNew老年代CMS,一般来说,我们会设置新生代的大小,以为默认情况下,有可能jvm默认的新生代很小,如果堆内存给了比较大的空间,新生代不做调整的话,浪费内存空间.

以下为我使用的jvm参数:

#使用ParNew+CMS垃圾回收器
-XX:+UseConcMarkSweepGC
#打印GC详情
-XX:+PrintGCDetails
#打印GC的日期
-XX:+PrintGCDateStamps
#打印GC时间
-XX:+PrintGCTimeStamps
#指定GC日志位置
-Xloggc:/opt/app/logs/gc.log
#指定最大堆内存
-Xmx2048M
#指定最小堆内存
-Xms2048M
#新生代和老年代的内存占比1:2
-XX:NewRatio=2
#Eden区和Survivor比例8:1 
-XX:SurvivorRatio=8
#指定新生代的最小内存空间
-XX:NewSize=715784192
#指定新生代的最大内存空间
-XX:MaxNewSize=715784192

使用以上JVM参数以后,我们查看gc日志如下(可以使用less 文件名的方式查看gc日志,PageUp向上翻页,PageDown向下翻页):

OpenJDK 64-Bit Server VM (25.212-b04) for linux-amd64 JRE (1.8.0_212-b04), built on May  4 2019 17:56:12 by "buildozer" with gcc 8.3.0
Memory: 4k page, physical 3072000k(3070320k free), swap 0k(0k free)
CommandLine flags: -XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=715784192 -XX:NewRatio=2 -XX:NewSize=715784192 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:Survivor
Ratio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
2021-04-29T16:37:58.684+0800: 5.799: [GC (Allocation Failure) 2021-04-29T16:37:58.684+0800: 5.799: [ParNew: 559232K->26278K(629120K), 0.0439056 secs] 559232K->26278K(2027264K), 0.0440200 secs] [Times: user=0.06 sys=0.01, real=0.04 secs] 
2021-04-29T16:37:58.728+0800: 5.843: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1398144K)] 41154K(2027264K), 0.0076770 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2021-04-29T16:37:58.736+0800: 5.851: [CMS-concurrent-mark-start]
2021-04-29T16:37:58.744+0800: 5.859: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
2021-04-29T16:37:58.744+0800: 5.859: [CMS-concurrent-preclean-start]
2021-04-29T16:37:58.747+0800: 5.862: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2021-04-29T16:37:58.748+0800: 5.862: [CMS-concurrent-abortable-preclean-start]
2021-04-29T16:37:59.594+0800: 6.708: [CMS-concurrent-abortable-preclean: 0.458/0.846 secs] [Times: user=1.59 sys=0.08, real=0.85 secs] 
2021-04-29T16:37:59.594+0800: 6.708: [GC (CMS Final Remark) [YG occupancy: 394027 K (629120 K)]2021-04-29T16:37:59.594+0800: 6.709: [Rescan (parallel) , 0.1164667 secs]2021-04-29T16:37:59.710+0800: 6.825: [weak refs processing, 0.0000352 secs]2021-0
4-29T16:37:59.710+0800: 6.825: [class unloading, 0.0041990 secs]2021-04-29T16:37:59.715+0800: 6.829: [scrub symbol table, 0.0038982 secs]2021-04-29T16:37:59.719+0800: 6.833: [scrub string table, 0.0005058 secs][1 CMS-remark: 0K(1398144K)] 394027K(20
27264K), 0.1260089 secs] [Times: user=0.29 sys=0.00, real=0.12 secs] 
2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-sweep-start]
2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-reset-start]
2021-04-29T16:37:59.791+0800: 6.906: [CMS-concurrent-reset: 0.071/0.071 secs] [Times: user=0.07 sys=0.05, real=0.07 secs] 
2021-04-29T16:38:00.341+0800: 7.455: [GC (Allocation Failure) 2021-04-29T16:38:00.341+0800: 7.455: [ParNew: 585510K->23331K(629120K), 0.0716625 secs] 585510K->23331K(2027264K), 0.0717529 secs] [Times: user=0.14 sys=0.02, real=0.07 secs] 
2021-04-29T16:38:01.715+0800: 8.829: [GC (Allocation Failure) 2021-04-29T16:38:01.715+0800: 8.829: [ParNew: 582563K->36089K(629120K), 0.0958375 secs] 582563K->36089K(2027264K), 0.0959258 secs] [Times: user=0.14 sys=0.00, real=0.09 secs] 
2021-04-29T16:38:02.687+0800: 9.801: [GC (Allocation Failure) 2021-04-29T16:38:02.687+0800: 9.801: [ParNew: 595321K->29885K(629120K), 0.0493737 secs] 595321K->29885K(2027264K), 0.0494595 secs] [Times: user=0.12 sys=0.01, real=0.05 secs] 
2021-04-29T16:38:03.564+0800: 10.679: [GC (Allocation Failure) 2021-04-29T16:38:03.564+0800: 10.679: [ParNew: 589117K->38018K(629120K), 0.0469700 secs] 589117K->38018K(2027264K), 0.0470567 secs] [Times: user=0.11 sys=0.00, real=0.04 secs] 
2021-04-29T16:38:04.721+0800: 11.835: [GC (Allocation Failure) 2021-04-29T16:38:04.721+0800: 11.835: [ParNew: 597250K->38175K(629120K), 0.1040785 secs] 597250K->38175K(2027264K), 0.1041795 secs] [Times: user=0.10 sys=0.01, real=0.11 secs] 
2021-04-29T16:38:04.825+0800: 11.940: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1398144K)] 39294K(2027264K), 0.0224121 secs] [Times: user=0.04 sys=0.00, real=0.02 secs] 
2021-04-29T16:38:04.848+0800: 11.962: [CMS-concurrent-mark-start]
2021-04-29T16:38:04.856+0800: 11.970: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
2021-04-29T16:38:04.856+0800: 11.971: [CMS-concurrent-preclean-start]
2021-04-29T16:38:04.860+0800: 11.974: [CMS-concurrent-preclean: 0.004/0.004 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
2021-04-29T16:38:04.860+0800: 11.975: [CMS-concurrent-abortable-preclean-start]
2021-04-29T16:38:05.711+0800: 12.826: [CMS-concurrent-abortable-preclean: 0.779/0.851 secs] [Times: user=1.65 sys=0.07, real=0.85 secs] 
2021-04-29T16:38:05.711+0800: 12.826: [GC (CMS Final Remark) [YG occupancy: 331082 K (629120 K)]2021-04-29T16:38:05.711+0800: 12.826: [Rescan (parallel) , 0.0991187 secs]2021-04-29T16:38:05.810+0800: 12.925: [weak refs processing, 0.0000489 secs]202
1-04-29T16:38:05.810+0800: 12.925: [class unloading, 0.0068616 secs]2021-04-29T16:38:05.817+0800: 12.932: [scrub symbol table, 0.0083112 secs]2021-04-29T16:38:05.826+0800: 12.940: [scrub string table, 0.0006307 secs][1 CMS-remark: 0K(1398144K)] 3310
82K(2027264K), 0.1164628 secs] [Times: user=0.19 sys=0.00, real=0.12 secs] 
2021-04-29T16:38:05.828+0800: 12.942: [CMS-concurrent-sweep-start]
2021-04-29T16:38:05.828+0800: 12.942: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2021-04-29T16:38:05.828+0800: 12.942: [CMS-concurrent-reset-start]
2021-04-29T16:38:05.831+0800: 12.946: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2021-04-29T16:38:06.290+0800: 13.405: [GC (Allocation Failure) 2021-04-29T16:38:06.290+0800: 13.405: [ParNew: 597407K->58488K(629120K), 0.1068891 secs] 597407K->58488K(2027264K), 0.1069796 secs] [Times: user=0.14 sys=0.01, real=0.11 secs] 
2021-04-29T16:38:07.295+0800: 14.409: [GC (Allocation Failure) 2021-04-29T16:38:07.295+0800: 14.409: [ParNew: 617720K->53644K(629120K), 0.1625714 secs] 617720K->68440K(2027264K), 0.1626508 secs] [Times: user=0.24 sys=0.02, real=0.16 secs] 
2021-04-29T16:38:08.389+0800: 15.504: [GC (Allocation Failure) 2021-04-29T16:38:08.389+0800: 15.504: [ParNew: 612876K->35515K(629120K), 0.0610254 secs] 627672K->50311K(2027264K), 0.0611143 secs] [Times: user=0.16 sys=0.01, real=0.06 secs] 
2021-04-29T16:38:09.449+0800: 16.563: [GC (Allocation Failure) 2021-04-29T16:38:09.449+0800: 16.564: [ParNew: 594747K->37068K(629120K), 0.0614502 secs] 609543K->51864K(2027264K), 0.0615418 secs] [Times: user=0.09 sys=0.00, real=0.06 secs] 
2021-04-29T16:38:10.525+0800: 17.639: [GC (Allocation Failure) 2021-04-29T16:38:10.525+0800: 17.639: [ParNew: 596300K->37786K(629120K), 0.0698999 secs] 611096K->52582K(2027264K), 0.0699826 secs] [Times: user=0.12 sys=0.00, real=0.07 secs] 
2021-04-29T16:38:11.173+0800: 18.288: [GC (Allocation Failure) 2021-04-29T16:38:11.173+0800: 18.288: [ParNew: 597018K->36129K(629120K), 0.0386147 secs] 611814K->50925K(2027264K), 0.0386905 secs] [Times: user=0.07 sys=0.00, real=0.04 secs] 
2021-04-29T16:38:12.205+0800: 19.320: [GC (Allocation Failure) 2021-04-29T16:38:12.205+0800: 19.320: [ParNew: 595361K->42277K(629120K), 0.0603755 secs] 610157K->57073K(2027264K), 0.0604587 secs] [Times: user=0.10 sys=0.00, real=0.06 secs] 
2021-04-29T16:38:13.308+0800: 20.423: [GC (Allocation Failure) 2021-04-29T16:38:13.308+0800: 20.423: [ParNew: 601509K->48883K(629120K), 0.0707038 secs] 616305K->63679K(2027264K), 0.0707979 secs] [Times: user=0.12 sys=0.00, real=0.07 secs] 
2021-04-29T16:38:14.230+0800: 21.344: [GC (Allocation Failure) 2021-04-29T16:38:14.230+0800: 21.344: [ParNew: 608115K->47267K(629120K), 0.0806798 secs] 622911K->68980K(2027264K), 0.0807631 secs] [Times: user=0.11 sys=0.01, real=0.08 secs] 

可以看到,在加入了以上的JVM参数以后,每次GC的时间都打印的很详细,我们可以清楚的知道每一次gc的情况.下面对这段日志进行分析.

image-20210430140944547

1

OpenJDK 64-Bit Server VM (25.212-b04) for linux-amd64 JRE (1.8.0_212-b04), built on May  4 2019 17:56:12 by "buildozer" with gcc 8.3.0
Memory: 4k page, physical 3072000k(3070320k free), swap 0k(0k free)
CommandLine flags: -XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=715784192 -XX:NewRatio=2 -XX:NewSize=715784192 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:Survivor
Ratio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 

这一段依旧是说明当前JVM使用的一些参数,还有版本什么的,不再多说

2

2021-04-29T16:37:58.684+0800: 5.799: [GC (Allocation Failure) 2021-04-29T16:37:58.684+0800: 5.799: [ParNew: 559232K->26278K(629120K), 0.0439056 secs] 559232K->26278K(2027264K), 0.0440200 secs] [Times: user=0.06 sys=0.01, real=0.04 secs] 
2021-04-29T16:37:58.728+0800: 5.843: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1398144K)] 41154K(2027264K), 0.0076770 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

整体的结构和之前的GC日志相差并不大.

  • 2021-04-29T16:37:58.684+0800: 5.799:

    这段标识当前GC的时间

  • GC (Allocation Failure)

    表明当前GC的原因,在这里发送了2次,第一次是由于对象分配失败,第二次是CMD的初始标记阶段.因为CMS回收,即伴随着一次YuongGC,会对内存中的对象进行标记,这一行就表明当前YuongGC是由于CMS的初始标记引起的.

  • [ParNew: 559232K->26278K(629120K), 0.0439056 secs]

    很明显,ParNew回收器是收集年轻代的垃圾,这里回收的就是年轻代的区域.

    这里是说,年轻代回收前是559M左右,回收后是26M左右,总共大小是629M左右.

    后面的0.0439056 secs表明GC的耗时

  • 559232K->26278K(2027264K), 0.0440200 secs

    这段依旧是整个堆内存的占用情况,分别是GC前—>GC后(整个堆的内存大小),耗费时间.

  • [Times: user=0.06 sys=0.01, real=0.04 secs]

    这段就不说了,上面已经说过了,是GC的三种时间

  • 2021-04-29T16:37:58.728+0800: 5.843: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1398144K)] 41154K(2027264K), 0.0076770 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

    这就是所谓的CMS的初始标记阶段.标记所有可以到达GcRoot的对象

    CMS-initial-mark: 0K(1398144K)] 41154K(2027264K)

    这一句说明,老年代的总大小是1398144K,在占用达到0K的时候触发了CMS

3

2021-04-29T16:37:58.736+0800: 5.851: [CMS-concurrent-mark-start]
2021-04-29T16:37:58.744+0800: 5.859: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
2021-04-29T16:37:58.744+0800: 5.859: [CMS-concurrent-preclean-start]
2021-04-29T16:37:58.747+0800: 5.862: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2021-04-29T16:37:58.748+0800: 5.862: [CMS-concurrent-abortable-preclean-start]
2021-04-29T16:37:59.594+0800: 6.708: [CMS-concurrent-abortable-preclean: 0.458/0.846 secs] [Times: user=1.59 sys=0.08, real=0.85 secs] 

这一段其实就是FullGC了,我们知道,使用了CMS垃圾回收器以,Old去的回收就是CMS来做的 .

  • 2021-04-29T16:37:58.736+0800: 5.851: [CMS-concurrent-mark-start]

    这一段表示CMS的并发标记阶段,在第一阶段中停止的线程将再次启动,并且在第一阶段中标记的对象可传递到的所有对象都标记在这里。

  • 2021-04-29T16:37:58.744+0800: 5.859: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]

    这一段是并发标记的时间位,可以看出并发标记的时间是0.008s

  • 2021-04-29T16:37:58.744+0800: 5.859: [CMS-concurrent-preclean-start] 2021-04-29T16:37:58.747+0800: 5.862: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

    这两条,上面的一条是标识当前的阶段是CMS的并发预清理阶段,下面一条记录了时间

  • 2021-04-29T16:37:58.748+0800: 5.862: [CMS-concurrent-abortable-preclean-start] 2021-04-29T16:37:59.594+0800: 6.708: [CMS-concurrent-abortable-preclean: 0.458/0.846 secs] [Times: user=1.59 sys=0.08, real=0.85 secs]

    这两条是可终止预清理的标记和持续的时间

4

2021-04-29T16:37:59.594+0800: 6.708: [GC (CMS Final Remark) [YG occupancy: 394027 K (629120 K)]2021-04-29T16:37:59.594+0800: 6.709: [Rescan (parallel) , 0.1164667 secs]2021-04-29T16:37:59.710+0800: 6.825: [weak refs processing, 0.0000352 secs]2021-0
4-29T16:37:59.710+0800: 6.825: [class unloading, 0.0041990 secs]2021-04-29T16:37:59.715+0800: 6.829: [scrub symbol table, 0.0038982 secs]2021-04-29T16:37:59.719+0800: 6.833: [scrub string table, 0.0005058 secs][1 CMS-remark: 0K(1398144K)] 394027K(2027264K), 0.1260089 secs] [Times: user=0.29 sys=0.00, real=0.12 secs] 

这一段比较重要,我们一起来看一下,首先这个阶段是CMS最后一个STW阶段,任务是完成标记整个老年代的所有存活对象.他的内容如下:

  • 021-04-29T16:37:59.594+0800

    执行的时间

  • GC (CMS Final Remark)

    表明当前是最终的收集阶段

  • [YG occupancy: 394027 K (629120 K)]

    年轻代的当前占用的情况和容量

  • 2021-04-29T16:37:59.594+0800: 6.709: [Rescan (parallel , 0.1164667 secs]

    完成存活对象的标记工作

  • 2021-04-29T16:37:59.710+0800: 6.825: [weak refs processing, 0.0000352 secs]

    这个阶段处理弱引用

  • 2021-04-29T16:37:59.710+0800: 6.825: [class unloading, 0.0041990 secs]

    卸载类所用的时间

  • 2021-04-29T16:37:59.715+0800: 6.829: [scrub symbol table, 0.0038982 secs]

    清理字符串,元数据之类的占用

  • [1 CMS-remark: 0K(1398144K)] 394027K(2027264K), 0.1260089 secs] [Times: user=0.29 sys=0.00, real=0.12 secs]

    这个阶段以后,老年代的占用内存大小和容量(1398144),以及整个堆的大小和容量(2027264)

    以及所耗时间.

5

2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-sweep-start]
2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-reset-start]
2021-04-29T16:37:59.791+0800: 6.906: [CMS-concurrent-reset: 0.071/0.071 secs] [Times: user=0.07 sys=0.05, real=0.07 secs] 
  • 2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-sweep-start]
    2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    

    这两句是说,开始清除垃圾对象,这个时候,程序不是STW状态(stop the word),后面一句是耗时

  • 2021-04-29T16:37:59.720+0800: 6.835: [CMS-concurrent-reset-start]
    2021-04-29T16:37:59.791+0800: 6.906: [CMS-concurrent-reset: 0.071/0.071 secs] [Times: user=0.07 sys=0.05, real=0.07 secs] 
    

    这里是说,重新设置CMS算法内部的数据结构,准备下一个周期.也就是老年代清理完毕后的初始化.

OK,基本上到这里以后,所有的内容应该都有说过了.

ParNew和CMS的日志解析就到这里了.下面是G1部分

3) G1 日志分析

因为我这篇博文的所有数据都是来源于实际的生产环境,但是目前还没有使用到G1,我也不想用本地的数据来验证,因此先空再这里,后面生产环境实际使用G1以后会更新.


7. 结语

其实现阶段有了K8S容器化技术以后,基本上小中型公司的JVM一般是不用做什么大的调整的.我文中所示的,也是我们目前生产环境K8S容器中的真实数据,技术的学习在于不断的积累和验证,关于JVM调优,这只是个开始,也不会结束.希望能记录下来与君共勉.

其他日志详细信息,可以参考一下来自Oracle官方的博客.

https://blogs.oracle.com/poonam/understanding-cms-gc-logs CMS

https://blogs.oracle.com/poonam/understanding-g1-gc-logs G1

个人公众号<橙耘自留地>日前已经开通,后续博主发布的文章都会一并更新到公众号,如有需要,自行查阅.

关于橙耘自留地,是我个人聚焦互联网技术栈学习分享的一个平台,创立之初是因为目前业内各种技术课程资料层次不齐,褒贬不一,有时候一门课花费高价买入,其实内含草包,有时偶尔低价得之,却又大有干货.因此我会根据大家的意见和评价,选择不同的技术栈去学习,一为提升我自己的技术,二为大家梳理出质量比较好的课程,以作参考.同时,相关的学习心得也会一并更新到博客和公众号.

qrcode_for_gh_c1462e34b232_344

;