【JVM系列7】如何通過分析GC日誌來進行JVM調優 前言 什麼時候會發生垃圾收集 怎麼拿到GC日誌 PS+PO日誌分析 CMS日誌分析 G1日誌分析 利用工具分析GC日誌

前言

上一篇,我們介紹了JVM參數以及jdk提供的一些常用工具的使用,並且結合一個OOM例子,簡單講述瞭如何利用工具來分析dump文件,那麼本篇文章,將會介紹一個如何分析GC日誌。

不同的垃圾收集器產生的GC日誌大致遵循了同一個規則,只是有些許不同,不過對於G1收集器的GC日誌和其他垃圾收集器有較大差別,話不多說,正式進入正文。。。

什麼時候會發生垃圾收集

首先我們來看一個問題,那就是什麼時候會發生垃圾回收?
在Java中,GC是由JVM自動完成的,根據JVM系統環境而定,所以時機是不確定的。 當然,我們可以手動進行垃圾回收, 比如調用System.gc()方法通知JVM進行一次垃圾回收,但是具體什麼時刻運行也無法控制。也就是說System.gc()只是通知要回收,什麼時候回收由JVM決定。
一般以下幾種情況會發生垃圾回收:
1、當Eden區或者S區不夠用時
2、老年代空間不夠用了時
3、方法區空間不夠用時
4、System.gc() 時

注意:可能有些人會以爲方法區是不會發生垃圾回收的,其實方法區也是會發生垃圾回收的,只不過大部分情況下,方法區發生垃圾回收之後效率不是很高,大部分內存都回收不掉,所以我們一般討論垃圾回收的時候也只討論堆內的回收

怎麼拿到GC日誌

發生GC之後,我們要分析GC日誌,當然就首先要拿到GC日誌,上一篇講述JVM參數分類及常用參數分析時有提到,打印GC日誌可以通過如下命令:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:D:\\gc.log

配置上之後啓動服務:

找到gc.log文件,注意,剛開始如果一次GC都沒發生日誌是空的,可以等到發生GC之後再打開:


從日誌上可以看出來,jdk1.8中默認使用的是Parallel Scavenge+Parallel Old收集器,當然我們也可以通過參數:

-XX:+PrintCommandLineFlags
1

進行打印:

PS+PO日誌分析

前面三行應該都能看懂:
第一行打印的是當前所使用的的HotSpot虛擬機及其對應版本號;
第二行打印的是操作系統相關的內存信息;
第三行打印的是當前Java服務啓動後鎖配置的參數信息:

CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=131854336 -XX:+ManagementServer -XX:MaxHeapSize=2109669376 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 

包括了堆空間打印,以及使用的垃圾收集器及我們自己配置的打印GC日誌相關參數等一些信息。

下面第4行開始纔是我們的GC日誌,我們把第4行還有第9行復製出來分析一下:

//第4行
2020-08-23T15:35:30.747+0800: 5.486: [GC (Allocation Failure) [PSYoungGen: 32768K->3799K(37888K)] 32768K->3807K(123904K), 0.1129986 secs] [Times: user=0.02 sys=0.00, real=0.11 secs] 
//第9行
2020-08-23T15:35:34.635+0800: 9.374: [Full GC (Metadata GC Threshold) [PSYoungGen: 5092K->0K(136192K)] [ParOldGen: 12221K->12686K(63488K)] 17314K->12686K(199680K), [Metaspace: 20660K->20660K(1067008K)], 0.0890985 secs] [Times: user=0.25 sys=0.00, real=0.09 secs] 
  • 1、最前面一個時間2020-08-23T15:35:30.747+0800代表的是垃圾回收發生的時間。
  • 2、緊接着下面的一個數字:5.486表示的是從Java虛擬機啓動以來經過的秒數。
  • 3、再往下一個GC (Allocation Failure)表示發生GC的原因,這裏是表示分配空間失敗而發生了GC。
  • 4、PSYoungGen,PS表示的是Parallel Scavenge收集器,YoungGen表示的是當前發生GC的地方是年輕代,注意,這裏不同收集器會有不同的名字,如ParNew收集器就會顯示爲ParNew等。
  • 5、中括號之內的一個數字32768K->3799K(37888K)這個表示的是:GC前當前內存區域使用空間->GC後當前內存區域使用的內存空間(當前區域的總內存空間)。從這裏可以看到,一次GC之後,大部分空間都被回收掉了。
  • 6、中括號之外的數字32768K->3807K(123904K)這個表示的是:GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆使用的總容量)
    這裏需要注意的是5和6中的這兩組數字相減得到的值一般是不相等的,這是因爲總空間下面還包括了老年代發生回收後釋放的空間大小,可能有人會覺得奇怪,這裏明明只有新生代發生了GC,爲什麼老年代會有空間釋放?
    不知道大家還記不記得我在前兩篇文章中提到了,S區如果空間不夠的話會利用擔保機制向老年代借用空間,所以借來的空間時可能被釋放的
  • 7、0.1129986 secs這個表示的是GC所花費時間,secs表示單位是秒。
  • 8、[Times: user=0.02 sys=0.00, real=0.11 secs] 這一部分並不是所有的垃圾收集器都會打印,user=0.02表示用戶態消耗的CPU時間,sys=0.00表示內核態消耗的CPU時間和操作從開始到結束所經過的牆鍾時間。
  • 9、最後再看看其他行ParOldGen表示Parallel Old收集器在回收老年代,Metaspace表示的是方法區(jdk1.7是永久代)
  • 10、我們看到第9行Full GC表示發生了Full GC,FullGC=Minor GC+Major GC+Metaspace GC,所以後面可以看到PSYoungGen,ParOldGen,Metaspace三個區域的回收信息,而且第9行對比非常明顯,新生代全部回收掉了,老年代回收了一小部分,而方法區一點都沒有回收掉,這也體現了三個區域內所存對象的區別。

牆鍾時間和cpu時間

牆鍾時間(Wall Clock Time)包括各種非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些不需要消耗CPU的時間。

CMS日誌分析

我們垃圾收集器切換爲CMS

-XX:+UseConcMarkSweepGC

注意,CMS也是一款老年代收集器,使用這個參數後新生代默認會使用ParNew收集器
然後重啓服務,等候垃圾回收之後,打開gc.log文件。


前面兩行和上面一樣,我們把第三行復製出來看看垃圾收集器是否切換成功:

CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=131854336 -XX:+ManagementServer -XX:MaxHeapSize=2109669376 -XX:MaxNewSize=348966912 -XX:MaxTenuringThreshold=6 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 

可以看到,CMS和ParNew兩個收集器都啓用了。打開第4行日誌:

2020-08-23T17:00:34.728+0800: 5.259: [GC (Allocation Failure) 2020-08-23T17:00:34.728+0800: 5.259: [ParNew: 34432K->3862K(38720K), 0.0185214 secs] 34432K->3862K(124736K), 0.0188547 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 

這裏的回收信息和上面一樣,也就是新生代名字不一樣,這裏叫ParNew。我們主要看看老年代CMS的GC日誌,我們把一個完成的老年代回收日誌複製出來:

2020-08-23T17:00:47.650+0800: 18.182: [GC (CMS Initial Mark) [1 CMS-initial-mark: 30298K(86016K)] 34587K(124736K), 0.0014342 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-08-23T17:00:47.651+0800: 18.183: [CMS-concurrent-mark-start]
2020-08-23T17:00:47.712+0800: 18.244: [CMS-concurrent-mark: 0.061/0.061 secs] [Times: user=0.13 sys=0.00, real=0.06 secs] 
2020-08-23T17:00:47.712+0800: 18.244: [CMS-concurrent-preclean-start]
2020-08-23T17:00:47.714+0800: 18.245: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-08-23T17:00:47.714+0800: 18.246: [CMS-concurrent-abortable-preclean-start]
2020-08-23T17:00:48.143+0800: 18.674: [GC (Allocation Failure) 2020-08-23T17:00:48.143+0800: 18.674: [ParNew: 38720K->4111K(38720K), 0.0101415 secs] 69018K->38573K(124736K), 0.0102502 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
2020-08-23T17:00:48.451+0800: 18.983: [CMS-concurrent-abortable-preclean: 0.274/0.737 secs] [Times: user=0.94 sys=0.13, real=0.74 secs] 
2020-08-23T17:00:48.451+0800: 18.983: [GC (CMS Final Remark) [YG occupancy: 23345 K (38720 K)]2020-08-23T17:00:48.451+0800: 18.983: [Rescan (parallel) , 0.0046112 secs]2020-08-23T17:00:48.456+0800: 18.987: [weak refs processing, 0.0006259 secs]2020-08-23T17:00:48.457+0800: 18.988: [class unloading, 0.0062187 secs]2020-08-23T17:00:48.463+0800: 18.994: [scrub symbol table, 0.0092387 secs]2020-08-23T17:00:48.472+0800: 19.004: [scrub string table, 0.0006408 secs][1 CMS-remark: 34461K(86016K)] 57806K(124736K), 0.0219024 secs] [Times: user=0.05 sys=0.01, real=0.02 secs] 
2020-08-23T17:00:48.473+0800: 19.005: [CMS-concurrent-sweep-start]
2020-08-23T17:00:48.489+0800: 19.020: [CMS-concurrent-sweep: 0.015/0.015 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
2020-08-23T17:00:48.489+0800: 19.020: [CMS-concurrent-reset-start]
2020-08-23T17:00:48.492+0800: 19.023: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

如果不瞭解CMS垃圾收集器工作機制的,可以點擊這裏先了解一下,因爲後面會涉及到CMS工作機制。

  • 1、CMS Initial Mark對應的是CMS工作機制的第一步初始標記,主要是尋找GCRoot對象
  • 2、中括號內10443K(86016K)表示的是當前區域已經使用大小和總空間大小
  • 3、中括號外13477K(124736K)表示的是堆內已使用空間大小和堆內總空間大小
  • 4、CMS-concurrent-mark-start這裏對應了CMS工作機制中的第二步併發標記。這個階段主要是根據GCRoot對象遍歷整個引用鏈
  • 5、再往後面一行CMS-concurrent-mark: 0.052/0.052 secs,這裏的兩個時間,第一個表示該階段持續的cpu時間和牆鍾時間
  • 6、後面的CMS-concurrent-preclean和CMS-concurrent-abortable-preclean對應了預清理和可中斷預清理階段
  • 7、CMS-concurrent-sweep-start對應最終標記,此階段需要STW,可以看到,在此階段前發生了一次Young GC,這是爲了減少STW時間。
  • 8、CMS-concurrent-sweep併發清除垃圾,CMS-concurrent-reset重置線程

G1日誌分析

切換到G1垃圾收集器:

-XX:+UseG1GC 

然後重啓服務,等待發生垃圾回收之後打開gc.log文件:


可以看到,這個文件相比較於其他垃圾收集器要複雜的多。我們還是先看下第3行,確認是否使用了G1收集器:

CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=131854336 -XX:+ManagementServer -XX:MaxHeapSize=2109669376 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation 

可以看到確實使用了G1收集器。我們找到一次完整的G1日誌,複製出來:

2020-08-23T18:44:39.787+0800: 2.808: [GC pause (G1 Evacuation Pause) (young), 0.0029103 secs]
   [Parallel Time: 1.9 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 2807.7, Avg: 2807.8, Max: 2807.8, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.3, Avg: 0.6, Max: 0.8, Diff: 0.5, Sum: 2.2]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.9, Avg: 1.2, Max: 1.4, Diff: 0.5, Sum: 4.6]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 2.5, Max: 4, Diff: 3, Sum: 10]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
      [GC Worker Total (ms): Min: 1.7, Avg: 1.8, Max: 1.8, Diff: 0.1, Sum: 7.1]
      [GC Worker End (ms): Min: 2809.5, Avg: 2809.5, Max: 2809.5, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 1.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.8 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 6144.0K(6144.0K)->0.0B(12.0M) Survivors: 0.0B->1024.0K Heap: 6144.0K(126.0M)->1520.0K(126.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 

[GC pause (G1 Evacuation Pause) (young), 0.0029103 secs]這裏表示發生GC的區域是Young區,後面就是總共耗費的時間。
注意:G1雖然在物理上取消了區域的劃分,但是邏輯上依然保留了,所以日誌中還是會顯示young,Full GC會用mixed來表示。
[Parallel Time: 1.9 ms, GC Workers: 4] 這句表示線程的並行時間和並行的線程數
[GC Worker Start (ms): Min: 2807.7, Avg: 2807.8, Max: 2807.8, Diff: 0.1]這個表示並行的每個線程的開始時間最小值,平均值和最大值以及時間差(Max-Min)。
後面就是一些具體的執行步驟,在這裏就不逐行去說明了,如果有興趣的可以點擊這裏進行了解。這裏面有非常詳細的解釋,不過是英文版本,但是大致應該能看得懂:

利用工具分析GC日誌

雖然說我們從日誌上能看懂GC日誌,但是如果需要進行調優,我們最關注的是2個點:

  • 1、吞吐量(Throughput)
    吞吐量=運行用戶代碼時間/(運行用戶代碼時間+GC時間)
  • 2、GC暫停時間(Pause Time)
    Stop The World時間

那麼我們直接從GC日誌上很難看出來這兩個指標,如果要靠自己計算去確認問題,我覺得這會是一個噩夢。所以同樣的,我們需要有工具來幫助我們分析,下面就介紹2款常用的工具。

gceasy

  • 1、打開官網地址:gceasy.io/
  • 2、上傳gc日誌

然後可以進入

主頁面:

  • 這裏已經幫我們把吞吐量和GC暫停時間統計出來了,當然還有其他指標也有統計,有了工具我們就可以對比指標來確認哪種收集器適合自己的系統了。

GCViewer

  • 1、下載gcviewer的jar包
  • 2、執行命令java -jar gcviewer-1.36-SNAPSHOT.jar


打開主界面:

點擊File–>Open File

在右邊的第一個Summary概要裏面可以看到吞吐量的統計。
切換到第三個標籤Pause:

可以查看到各種停頓時間的統計。

總結

本文主要介紹了常用的垃圾收集器的GC日誌應該如何進行分析,並且介紹了兩款常用的工具來幫助我們更好更直觀的分析GC日誌。

下一篇,將會針對一些面試常見的而前面文章中又沒有提到的一些經典問題進行講解。

請關注我,一起學習進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章