1. 程式人生 > >手撕 JVM 垃圾收集日誌

手撕 JVM 垃圾收集日誌

下圖是本篇的寫作大綱,將從以下四個方面介紹怎麼樣處理 JVM 日誌。

有準備才能不慌

想要分析日誌,首先你得有日誌呀,對不對。凡是未雨綢蒙總是沒錯的。所謂有日誌的意思,你要把 JVM 引數配置好,日誌格式、日誌儲存位置等引數,當然了,除了日誌相關引數外,其他的一些必要引數最好也配置上,比如 HeapDump 。

我相信大部分成熟的專案都會配置 JVM 引數。但是還是有一些小專案真的會忽略。以至於 JVM 崩潰的時候不方便查詢問題原因而追悔莫及。比如下面這位同學(純屬虛構),雖然對話是虛構,但是是真的有不配置引數的。

要配置,有防備,不後悔;不配置,不準備,會追悔。

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/Users/fengzheng/jvmlog/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/fengzheng/jvmlog

以上可以說是配置 JVM 日誌以及 dump 現場最基本的配置了。這裡只介紹關於日誌的引數,其他的引數可以讀一下 JVM 你不可不知的引數

加了以上配置的 JVM 就像是一臺裝有黑匣子的飛機。

一個重要概念

併發(Concurrent)和並行(Parallel)在 JVM 垃圾收集過程中的定義有很多同學搞不清楚。所以沒次讀到這兩個概念的時候都一頭霧水。
併發(Concurrent):指垃圾收集執行緒和使用者執行緒可以同時進行。也就是說 JVM 在進行垃圾收集的時候,使用者還是可以正常的使用應用系統提供的服務。(當然了,並沒有一種完全併發的垃圾收集器,只是說在垃圾收集的大部分階段是併發的)

並行(Parallel):指垃圾收集器是多執行緒工作的,比方說有4個執行緒同時進行垃圾收集的工作,但是在收集的過程中,使用者執行緒是被掛起的。也就是在進行並行收集的時候,使用者無法正常使用應用系統提供的服務。

分析背景

本篇的全部內容都基於 JDK 8 Hotspot JVM,分別從 ParallelGC 、CMS、G1 三種常用的垃圾收集器來分析。可以通過下圖檢視三種垃圾收集器的對應關係,分別對應圖中標示的 1、2、3。

為了方便日誌分析,我設定了一下簡單的 JVM 作為基礎引數,其中年輕代 10M,老年代 10M,堆大小 20M。

-Xms20M
-Xmx20M
-Xmn10M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=6M
-XX:MaxMetaspaceSize=6M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/fengzheng/jvmlog

簡單日誌格式

通過引數 -verbose:gc 或者 -XX:+PrintGC 可以讓 JVM 開啟簡單日誌格式,對於這幾種垃圾收集期,簡單日誌的格式都是一致的。簡單日誌內容很少,只有GC型別(標示是 Minor GC 還是 Full GC)、GC 原因、堆收集前和收集後大小、堆的總大小以及收集耗時。

下面是簡單日誌配置下的幾條收集日誌,包括正常的空間分配失敗引起的收集、System.gc() 觸發的垃圾收集、以及執行 jmap -histo:live pid 命令執行的垃圾收集。

# Minor GC 新生代 GC
[GC (Allocation Failure)  7164K->704K(19456K), 0.0017002 secs]

# System.gc() 觸發 Full GC
[GC (System.gc())  4157K->648K(19456K), 0.0019522 secs]
[Full GC (System.gc())  648K->609K(19456K), 0.0099904 secs]

# jmap -histo:live 觸發 Full GC
[GC (Heap Inspection Initiated GC)  938K->737K(19456K), 0.0009119 secs]
[Full GC (Heap Inspection Initiated GC)  737K->573K(19456K), 0.0070892 secs]

下圖說明了一條簡單格式的垃圾收集日誌各個欄位的含義。

在實際的生產環境中,只用簡單格式的 JVM 日誌意義不大,得到的有用資訊不多,也就是知道垃圾收集次數、收集耗時以及堆的使用量,對於排查分析問題的幫助不是很大,所以,一般都會配置更加詳細的日誌格式。

詳細日誌格式

使用 -XX:+PrintGCDetails-XX:+PrintGCDateStamps 這兩個引數可以列印詳細的垃圾收集日誌和垃圾收集的時間戳。當然了,除了這兩個之外,還有一些更具體的引數,比如收集前後列印堆使用資訊的 -XX:+PrintHeapAtGC引數等等。

當然了,引數配置的越多,列印的資訊越是詳細,對於排查問題越有幫助,就是內容就會變得很多,肉眼看起來會比較抓狂。

Parallel Scavenge 收集器

在 JDK 8 中,如果不指定垃圾收集器,預設是使用引數 -XX:+UseParallelGC 的,也就是新生代使用 Parallel Scavenge,老年代配合使用的是 Serial Old。

Parallel Scavenge是一款並行的、高吞吐量的垃圾收集器,採用複製演算法。適用於追求高效率的、對即時響應要求不高的系統。

要了解清楚 GC 日誌各部分的含義,就要了解 JVM 記憶體模型以及垃圾收集器對於記憶體的規劃和管理情況,老樣子,還是通過圖來看一下比較清楚。JDK 8 支援的除 G1 外的垃圾收集器,都適用此圖,包括下面要介紹的 CMS。

垃圾收集的部分即是上圖中的「方法區」和 「堆」兩部分。收集日誌也基本上是描述這兩部分的大小和變化情況。

在上面的背景介紹中給出了本次測試所用的引數。年輕代 10M ,老年代 10M,Metaspace 區 6M。下圖是堆空間記憶體分佈圖,年輕代分為 Eden區和 S0、S1 兩個區,SurvivorRatio為8,這也是預設值,表示新生代 Eden 佔年輕代總大小的 80%,也就是 10*80%=8M,而 S0、S1 各佔10%,也就是 1M。

好了,基於上面的基礎認識。開始分析垃圾收集日誌,以下是兩條日誌,第一條是一次 Minor GC,第二條是 Full GC。

2019-12-03T16:20:47.980-0800: [GC (System.gc()) [PSYoungGen: 4068K->656K(9216K)] 4076K->672K(19456K), 0.0016106 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-12-03T16:20:47.982-0800: [Full GC (System.gc()) 
            [PSYoungGen: 656K->0K(9216K)],
        [ParOldGen: 16K->570K(10240K)] 672K->570K(19456K), 
        [Metaspace: 3910K->3910K(1056768K)],
 0.0110117 secs] 
 [Times: user=0.02 sys=0.00, real=0.01 secs] 

為了更清楚的說明各個部分的含義,我居然又畫了一張圖(PS:畫個圖真是不容易),看一下各部分代表的含義。

[^]: 紅色線框描述各個部分的含義

上圖標註的是一條 Full GC 日誌,Full GC 同時收集了年輕代、老年代以及 metaspace 區。Full GC 日誌包含了 Minor GC 的內容,那我們就直接分析 Full GC 了。

時間戳:日誌以時間戳作為開端,表示此次垃圾收集發生的時間,由 -XX:+PrintGCDateStamps 引數決定是否開啟。

收集內容主體:

沿著日誌順序往後看,Full GC (System.gc()),收集型別(是 Full GC 還是 Minor GC) ,括號裡跟著發生此次垃圾收集的原因。

再後面是年輕代、老年代、Metaspace 區詳細的收集情況。

[PSYoungGen: 656K->0K(9216K)],翻譯為 「年輕代:年輕代收集前記憶體使用量->年輕代垃圾收集後記憶體使用量(年輕代可用記憶體總大小)」,垃圾收集前年輕代已使用 656K,垃圾收集後已使用 0K,說明被回收了 656K,總可用大小為 9216K(9M)。誒,不對呀?怎麼是 9M 呢,年輕代不是分了 10 M 嗎。因為可用記憶體和總記憶體不能劃等號,S0 和 S1 只能有一塊被算進可用記憶體,所以可用記憶體為 Eden + S0/S1=9M。

[ParOldGen: 16K->570K(10240K)] 672K->570K(19456K),翻譯為 「[老年代:老年代收集前記憶體使用量->老年代垃圾收集後記憶體使用量(老年代可用記憶體總大小)] 堆空間(包括年輕代和老年代)垃圾收集前記憶體使用量->堆空間垃圾收集後記憶體使用量(堆空間總可用大小)」。

垃圾收集前老年使用 16K,收集後呢,竟然變大了,確定沒有看錯嗎。是的,沒有。這是因為年輕代的物件有一些進入了老年代導致的。老年代 16K 變成了 570K,說明有 554K 是年輕代晉升而來的。而記憶體總大小由 672K 減少到了 570K,說明有102K的記憶體真正的被清理了。

[Metaspace: 3910K->3910K(1056768K)]翻譯為元空間回收前大小為 3910K,回收後大小為3910K,總可用大小為 1056768K。我們不是設定的 6M 嗎,怎麼這麼大,沒起作用嗎。實際上這個值是 **CompressedClassSpaceSize +(2*InitialBootClassLoaderMetaspaceSize) **的大小,我們只設置了 MaxMetaspaceSize ,並沒有設定這兩個引數。使用如下命令可以看到這兩個值的預設大小

jinfo -flag CompressedClassSpaceSize 75867
-XX:CompressedClassSpaceSize=1073741824
jinfo -flag InitialBootClassLoaderMetaspaceSize 75867
-XX:InitialBootClassLoaderMetaspaceSize=4194304

單位是 byte,CompressedClassSpaceSize 的值是 1048576K(其實就是1G,預設值),InitialBootClassLoaderMetaspaceSize的值是 4M,用上面的公式計算,正好是 1056768K(1032M)

耗時統計

[Times: user=0.02 sys=0.00, real=0.01 secs]

user=0.02 表示執行使用者態程式碼的耗時,這裡也就是 GC 執行緒消耗的 CPU 時間。如果是多執行緒收集器,這個值會高於 real 時間。

sys=0.00 表示執行核心態程式碼的耗時。

real=0.01 表示應用停頓時長,多執行緒垃圾收集情況下,此數值應該接近(user + sys) / GCThreads(收集執行緒數),即單核上的平均停頓時間。

CMS 收集器

CMS 是一款老年代垃圾收集器,年輕代使用 ParNew 與之配合使用。它是一款併發、低停頓的垃圾收集器。適用於要求低延遲、即時響應的應用系統。

CMS 規劃的記憶體模型和上面 Parallel Scavenge 的是一致的,可以參考上面的記憶體分佈圖。

CMS 採用標記-清除演算法,演算法過程比較複雜,分為一下幾個步驟:

  • 初始標記(CMS initial mark),會導致 stop the world;

  • 併發標記(CMS concurrent mark),與使用者執行緒同時執行;

  • 預清理(CMS-concurrent-preclean),與使用者執行緒同時執行;

  • 可被終止的預清理(CMS-concurrent-abortable-preclean) 與使用者執行緒同時執行;

  • 重新標記(CMS remark),會導致 stop the world;

  • 併發清除(CMS concurrent sweep),與使用者執行緒同時執行;

  • 併發重置狀態等待下次CMS的觸發(CMS-concurrent-reset),與使用者執行緒同時執行;

只有初始標記和重新標記這兩個步驟會導致 STW,但是這兩個步驟耗時很短,其他步驟可以與使用者執行緒同時執行,所以使用者幾乎感覺不到 JVM 停頓。

使用引數 -XX:+UseConcMarkSweepGC可啟用 CMS 垃圾收集器。更詳細的引數如下:

-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+CMSClassUnloadingEnabled
-XX:+ParallelRefProcEnabled
# 在重新標記之前對年輕代做一次minor GC
-XX:+CMSScavengeBeforeRemark

使用了-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses-XX:+ExplicitGCInvokesConcurrent引數,在進行 Full GC 的時候,比如執行 System.gc() 操作,會觸發 CMS GC,以此來提高 GC 效率。

以下是啟用 CMS 後摘的一段 GC 日誌,由於內容過長,下面我就直接在日誌上做註釋了。

# System.gc() 觸發一次 Full GC
# -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 引數
# 導致Full GC 以 CMS GC 方式執行
# 先由 ParNew 收集器回收年輕代
2019-12-03T16:43:03.179-0800: [GC (System.gc()) 2019-12-03T16:43:03.179-0800: [ParNew: 3988K->267K(9216K), 0.0091869 secs] 3988K->919K(19456K), 0.0092257 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

# 初始標記階段,標記那些直接被 GC root 引用或者被年輕代存活物件所引用的所有物件
# 老年代當前使用 651K
# 老年代可用大小 10240K=10M
# 當前堆記憶體使用量 919K
# 當前堆可用記憶體 19456K=19M
# “1 CMS-initial-mark” 這裡的 1 表示老生代
2019-12-03T16:43:03.189-0800: [GC (CMS Initial Mark) [1 CMS-initial-mark: 651K(10240K)] 919K(19456K), 0.0002156 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

# 併發標記開始
# 標記所有存活的物件,它會根據上個階段找到的 GC Roots 遍歷查詢
2019-12-03T16:43:03.189-0800: [CMS-concurrent-mark-start]

# 併發標記階段耗時統計
2019-12-03T16:43:03.190-0800: [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

# 併發預清理階段開始
# 在上述併發標記過程中,一些物件的引用可能會發生變化,JVM 會將包含這個物件的區域(Card)標記為 Dirty
# 在此階段,能夠從 Dirty 物件到達的物件也會被標記,這個標記做完之後,dirty card 標記就會被清除了
2019-12-03T16:43:03.190-0800: [CMS-concurrent-preclean-start]

# 併發預清理耗時統計
2019-12-03T16:43:03.190-0800: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

# 重新標記階段,目的是完成老年代中所有存活物件的標記
# 上一階段是併發執行的,在執行過程中物件的引用關係還會發生變化,所以再次標記
# 因為配置了 -XX:+CMSScavengeBeforeRemark 引數,所以會在標記發生一次 Minor GC
# 進行一次Minor GC,完成後年輕代可用空間 267K,年輕代總大小9216K
2019-12-03T16:43:03.190-0800: [GC (CMS Final Remark) [YG occupancy: 267 K (9216 K)]
# 更詳細的年輕代收集情況
2019-12-03T16:43:03.190-0800: [GC (CMS Final Remark) 2019-12-03T16:43:03.190-0800: [ParNew: 267K->103K(9216K), 0.0021800 secs] 919K->755K(19456K), 0.0022127 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
# 在程式暫停時重新進行掃描(Rescan),以完成存活物件的標記
2019-12-03T16:43:03.192-0800: [Rescan (parallel) , 0.0002866 secs]
# 第一子階段:處理弱引用
2019-12-03T16:43:03.193-0800: [weak refs processing, 0.0015605 secs]
# 第二子階段:解除安裝不適用的類
2019-12-03T16:43:03.194-0800: [class unloading, 0.0010847 secs]
# 第三子階段:清理持有class級別 metadata 的符號表(symbol tables),以及內部化字串對應的 string tables
# 完成後老年代使用量為651K(老年代總大小10240K=10M)
# 整個堆使用量 755K(總堆大小19456K=19M)
2019-12-03T16:43:03.195-0800: [scrub symbol table, 0.0015690 secs]
2019-12-03T16:43:03.197-0800: [scrub string table, 0.0003786 secs][1 CMS-remark: 651K(10240K)] 755K(19456K), 0.0075058 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]

#開始併發清理 清除未被標記、不再使用的物件以釋放記憶體空間
2019-12-03T16:43:03.198-0800: [CMS-concurrent-sweep-start]
#併發清理階段耗時
2019-12-03T16:43:03.198-0800: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

# 開始併發重置,重置CMS演算法相關的內部資料, 為下一次GC迴圈做準備
2019-12-03T16:43:03.198-0800: [CMS-concurrent-reset-start]
# 重置耗時
2019-12-03T16:43:03.199-0800: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

# 下面是執行 jmap -histo:live 命令觸發的 Full GC
# GC 型別是 Full GC
# 觸發原因是 Heap Inspection Initiated GC
# CMS收集老年代:從清理前的650K變為清理後的617K,總的老年代10M,耗時0.0048490秒
# 總堆使用大小由 1245K變為617K,總堆19M
# metaspace: 3912K變為3912K,
# metaspace 總大小顯示為  CompressedClassSpaceSize +(2*InitialBootClassLoaderMetaspaceSize)
2019-12-03T16:43:20.115-0800: [Full GC (Heap Inspection Initiated GC) 2019-12-03T16:43:20.115-0800: [CMS: 650K->617K(10240K), 0.0048490 secs] 1245K->617K(19456K), [Metaspace: 3912K->3912K(1056768K)], 0.0049050 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

以上就是對 CMS 垃圾收集器產生日誌的分析,因為過程複雜,所以產生的日誌內容也比較多。

G1 收集器

G1 收集器是在 JDK 7 版本中就已經正式推出,並且作為 JDK 9 預設的垃圾收集器。

Parallel Scavenge:我追求高吞吐量,現在社會什麼最重要,效率呀,有沒有。

CMS:效率固然重要,極致的使用者體驗才是王道啊,不能讓使用者等啊,不能等啊,低停頓、即時響應是我畢生追求。

G1(一臉不屑):有句話不只當講不當講,首先宣告沒有惡意,我想說,在座的各位都是垃圾。上面兩位說的,我全都有,是的,全都有。 (ps:結果被打)

以上純屬開個玩笑,只是為了說明 G1 在滿足了低停頓的同時也保證了高吞吐量,適用於多核處理器、大記憶體容量的服務端系統。

G1 是 CMS 的替代版本,具有如下特點:

  • 橫跨年輕代和老年代,不需要其他收集器配合;
  • 併發收集器,可以與使用者執行緒併發執行;
  • 會壓縮記憶體碎片;
  • 可預測的停頓時間與高吞吐量;

與其他的垃圾收集器不同,G1 對堆記憶體做了不一樣的規劃,雖然還是使用分代策略,分為老年代、年輕代,年輕代又分為 Eden、Survivior 區,但是隻是邏輯劃分,物理上並不連續。它是將堆記憶體分為一系列大小在1M-32M 不等的 Region 區,通過下方的圖可以直觀的看出效果。

G1 垃圾收集包括年輕代收集和老年代收集兩部分。
年輕代比較簡單,收集器如果檢測到存活區物件存活時間達到閾值,就會將這些存活物件轉移到新的 Survivor 區或老年代,此過程會導致 stop the world。
老年代的收集就比較複雜了,包括如下幾個階段:

  • 初始標記階段(Initial Marking Phase),會導致 stop the wrold;
  • 根區域掃描(Root Region Scan),與應用程式併發執行;
  • 根區域掃描(Root Region Scan),與應用程式併發執行;
  • 併發標記(Concurrent Marking),與應用程式併發執行;
  • 最終標記(Remark),會導致 stop the wrold;
  • 複製/清除(Copying/Cleanup),會導致 stop the wrold;

開啟 G1 收集器的引數如下:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=100

使用 G1 收集器時,一般不設定年輕代的大小。

以下是一次 G1 收集的日誌,簡單的分析直接寫到下面的日誌內了。

# 進行了一次年輕代 GC,耗時0.0008029S
[GC pause (G1 Humongous Allocation) (young), 0.0008029 secs]
# 4個GC執行緒並行執行
   [Parallel Time: 0.5 ms, GC Workers: 4]
   # GC 執行緒耗時統計,反應收集的穩定性和效率
      [GC Worker Start (ms): Min: 90438.1, Avg: 90438.2, Max: 90438.4, Diff: 0.3]
      # 掃描堆外記憶體耗時統計
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.2, Diff: 0.2, Sum: 0.6]
      # 更新和掃描RSets 耗時統計
      [Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.2]
         [Processed Buffers: Min: 0, Avg: 0.2, Max: 1, Diff: 1, Sum: 1]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      #掃描堆中的 root 物件耗時統計
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      # 拷貝存活物件耗時統計
      [Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      # GC 執行緒確保自身安全停止耗時統計
      [Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.5]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      # GC的worker 執行緒的工作時間總計
      [GC Worker Total (ms): Min: 0.1, Avg: 0.4, Max: 0.5, Diff: 0.3, Sum: 1.5]
      # GC的worker 執行緒完成作業的時間統計
      [GC Worker End (ms): Min: 90438.6, Avg: 90438.6, Max: 90438.6, Diff: 0.0]
   # 修復GC期間code root指標改變的耗時
   [Code Root Fixup: 0.0 ms]
   # 清除code root耗時
   [Code Root Purge: 0.0 ms]
   # 清除card tables 中的dirty card的耗時
   [Clear CT: 0.0 ms]
   # 其他方面比如選擇CSet、處理已用物件、引用入ReferenceQueues、釋放CSet中的region等的耗時
   [Other: 0.3 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   # 收集前 Eden區使用量 1024K(總容量9216K),收集後容量0B(總容量9216K)
   # Survivors 區收集前後的大小
   # 堆空間收集前使用量13.4M(總量20M),收集後650.2K
   [Eden: 1024.0K(9216.0K)->0.0B(9216.0K) Survivors: 1024.0K->1024.0K Heap: 13.4M(20.0M)->650.2K(20.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 

# 初始標記階段,耗時0.0031800s
2019-12-03T16:48:25.456-0800: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0031800 secs][Parallel Time: 2.5 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 4115.2, Avg: 4115.4, Max: 4115.8, Diff: 0.6]
      ...
   [Eden: 3072.0K(10.0M)->0.0B(9216.0K) Survivors: 0.0B->1024.0K Heap: 9216.0K(20.0M)->744.0K(20.0M)]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
 
# Root區掃描 
2019-12-03T16:48:25.460-0800: [GC concurrent-root-region-scan-start]
2019-12-03T16:48:25.462-0800: [GC concurrent-root-region-scan-end, 0.0024198 secs]
# 併發標記
2019-12-03T16:48:25.462-0800: [GC concurrent-mark-start]
2019-12-03T16:48:25.462-0800: [GC concurrent-mark-end, 0.0001306 secs]

# 再次標記
2019-12-03T16:48:25.462-0800: [GC remark 2019-12-03T16:48:25.462-0800: [Finalize Marking, 0.0015922 secs] 2019-12-03T16:48:25.464-0800: [GC ref-proc, 0.0004899 secs] 2019-12-03T16:48:25.465-0800: [Unloading, 0.0016093 secs], 0.0040544 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
# 清理工作 
2019-12-03T16:48:25.467-0800: [GC cleanup 4000K->4000K(20M), 0.0003710 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 

其他工具

大多數時候靠肉眼分析日誌是很困難的,藉助一些分析工具是必不可少的。

線上日誌分析網站:https://gceasy.io/
線上 JVM 引數生成:https://opts.console.perfma.com/result/generate/JlKEV

【【【【【【【 寫文不易,瘋狂求推薦 】】】】】】

還可以讀:

JVM 你不可不知的引數

無意中就做了個 web 版 JVM 監控端

JConsole、VisualVM 依賴的 JMX 技術

Java 除錯工具、熱部署、JVM 監控工具都用到了它

我們說的 JVM 記憶體是什麼

分析 JVM 常用的 JDK 內建工具

歡迎關注,不定期更新本系列和其他文章
古時的風箏 ,進入公眾號可以加入交流群