GC 日志詳解,助你定位 GC 相關(guān)問題
點擊下方“IT牧場”,選擇“設(shè)為星標”

生產(chǎn)環(huán)境上,或者其他要測試 GC 問題的環(huán)境上,一定會配置上打印GC日志的參數(shù),便于分析 GC 相關(guān)的問題。
但是可能很多人配置的都不夠“完美”,要么是打印的內(nèi)容過少,要么是輸出到控制臺,要么是一個大文件被覆蓋,要么是……
本文帶你一步一步,配置一個完美的 GC 日志打印策略
打印內(nèi)容
為了保留足夠多的“現(xiàn)場證據(jù)”,最好是把 GC 相關(guān)的信息打印的足夠完整。而且你的程序真的不差你GC時打印日志I/O消耗的那點性能
打印基本 GC 信息
打印 GC 日志的第一步,就是開啟 GC 打印的參數(shù)了,也是最基本的參數(shù)。
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
打印對象分布
為了分析 GC 時的晉升情況和晉升導致的高暫停,不看對象年齡分布日志怎么行
-XX:+PrintTenuringDistribution
輸出內(nèi)容示例:
Desired survivor size 59244544 bytes, new threshold 15 (max 15)
- age 1: 963176 bytes, 963176 total
- age 2: 791264 bytes, 1754440 total
- age 3: 210960 bytes, 1965400 total
- age 4: 167672 bytes, 2133072 total
- age 5: 172496 bytes, 2305568 total
- age 6: 107960 bytes, 2413528 total
- age 7: 205440 bytes, 2618968 total
- age 8: 185144 bytes, 2804112 total
- age 9: 195240 bytes, 2999352 total
- age 10: 169080 bytes, 3168432 total
- age 11: 114664 bytes, 3283096 total
- age 12: 168880 bytes, 3451976 total
- age 13: 167272 bytes, 3619248 total
- age 14: 387808 bytes, 4007056 total
- age 15: 168992 bytes, 4176048 total
GC 后打印堆數(shù)據(jù)
每次發(fā)生 GC 時,對比一下 GC 前后的堆內(nèi)存情況,更直觀
-XX:+PrintHeapAtGC
輸出內(nèi)容示例:
{Heap before GC invocations=0 (full 0):
garbage-first heap total 1024000K, used 324609K [0x0000000781800000, 0x0000000781901f40, 0x00000007c0000000)
region size 1024K, 6 young (6144K), 0 survivors (0K)
Metaspace used 3420K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 371K, capacity 388K, committed 512K, reserved 1048576K
Heap after GC invocations=1 (full 1):
garbage-first heap total 1024000K, used 21755K [0x0000000781800000, 0x0000000781901f40, 0x00000007c0000000)
region size 1024K, 0 young (0K), 0 survivors (0K)
Metaspace used 3420K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 371K, capacity 388K, committed 512K, reserved 1048576K
}
打印 STW 時間
暫停時間是 GC 最重要的指標,肯定不能少
-XX:+PrintGCApplicationStoppedTime
輸出內(nèi)容示例:
Total time for which application threads were stopped: 0.0254260 seconds, Stopping threads took: 0.0000218 seconds
打印 safepoint 信息
進入STW階段之前,需要要找到一個合適的 safepoint ,這個指標一樣很重要(非必選,出現(xiàn) GC 問題時最好加上此參數(shù)調(diào)試)
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1
輸出內(nèi)容示例:
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.371: ParallelGCFailedAllocation [ 10 0 0 ] [ 0 0 0 0 7 ] 0
Execute full gc...dataList has been promoted to cms old space
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.379: ParallelGCSystemGC [ 10 0 0 ] [ 0 0 0 0 16 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.396: no vm operation [ 9 1 1 ] [ 0 0 0 0 341 ] 0
打印 Reference 處理信息
強引用/弱引用/軟引用/虛引用/finalize 方法萬一有問題,不得打印出來看看?
-XX:+PrintReferenceGC
輸出內(nèi)容示例:
2021-02-19T12:41:30.462+0800: 5072726.605: [SoftReference, 0 refs, 0.0000521 secs]
2021-02-19T12:41:30.462+0800: 5072726.605: [WeakReference, 0 refs, 0.0000069 secs]
2021-02-19T12:41:30.462+0800: 5072726.605: [FinalReference, 0 refs, 0.0000056 secs]
2021-02-19T12:41:30.462+0800: 5072726.605: [PhantomReference, 0 refs, 0 refs, 0.0000059 secs]
2021-02-19T12:41:30.462+0800: 5072726.605: [JNI Weak Reference, 0.0000131 secs], 0.4635293 secs]
完整參數(shù)
# requireds
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintHeapAtGC
-XX:+PrintReferenceGC
-XX:+PrintGCApplicationStoppedTime
# optional
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
輸出方式
上面只是定義了打印的內(nèi)容,默認情況下,這些日志會輸出到控制臺(標準輸出)。那如果你的程序日志也輸出到控制臺呢,這個日志內(nèi)容就會很亂,分析起來很麻煩。如果你是追加的方式(比如 tomcat 的 catalina.out 就是追加),這個文件會越來越大,分析起來就要命了。
所以需要一種分割日志的機制,這個機制嘛……JVM自然是提供的。
JVM 的日志分割
JVM提供了幾個用于分割 GC 日志的參數(shù):
# GC日志輸出的文件路徑
-Xloggc:/path/to/gc.log
# 開啟日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割幾個文件,超過之后從頭開始寫
-XX:NumberOfGCLogFiles=14
# 每個文件上限大小,超過就觸發(fā)分割
-XX:GCLogFileSize=100M
按照這個參數(shù),每個GC日志只要超過20M就會進行分割,最多分割5個文件,文件名依次是gc.log.0,gc.log.1,gc.log.2,gc.log.3,gc.log.4, .....
看似很美好,幾行配置就搞定了輸出文件的問題。但是這種方式有一些問題:
-Xloggc 方式指定的日志文件,是覆蓋寫的方式,每次啟動都會覆蓋,歷史日志會丟失 當超過最大分割數(shù)后,會從第0個文件開始重新寫入,而且是覆蓋 -XX:NumberOfGCLogFiles 并不能設(shè)置為無限
這個覆蓋的問題就有點惡心了,每次啟動覆蓋之前的歷史日志……這誰能忍?
使用時間戳命名文件
于是有另一種解決方案。不使用 JVM 提供的日志分割功能,而是每次啟動用時間戳命名日志文件,這樣可以每次啟動都使用不同的文件,就不會出現(xiàn)覆蓋的問題了。
# 使用-%t作為日志文件名
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc-%t.log
# 生成的文件名是這種:gc-2021-03-29_20-41-47.log
可是這樣就完美嗎?
雖然沒有覆蓋的問題,但由于沒有日志分割的功能,每次啟動后只有一個GC日志文件,單個日志文件可能會非常巨大。過大的日志文件分析起來是很麻煩的,必須得分割。
二者結(jié)合
這里只需要稍微調(diào)整一下策略,將 JVM 分割和時間戳命名兩種方案結(jié)合,就可以得到最優(yōu)的方式了。
# GC日志輸出的文件路徑
-Xloggc:/path/to/gc-%t.log
# 開啟日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割幾個文件,超過之后從頭開始寫
-XX:NumberOfGCLogFiles=14
# 每個文件上限大小,超過就觸發(fā)分割
-XX:GCLogFileSize=100M
配置時間戳作文 GC 日志文件名的同時,也配置JVM的GC日志分割策略。這樣一來,既保證了 GC 文件不會被覆蓋,又保證了單個 GC 文件的大小不會過大,完美!
最終得到的日志文件名會像這個樣子:
gc-2021-03-29_20-41-47.log.0 gc-2021-03-29_20-41-47.log.1 gc-2021-03-29_20-41-47.log.2 gc-2021-03-29_20-41-47.log.3 ....
最佳實踐 - 完整參數(shù)
# 必備
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintHeapAtGC
-XX:+PrintReferenceGC
-XX:+PrintGCApplicationStoppedTime
# 可選
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志輸出的文件路徑
-Xloggc:/path/to/gc-%t.log
# 開啟日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割幾個文件,超過之后從頭文件開始寫
-XX:NumberOfGCLogFiles=14
# 每個文件上限大小,超過就觸發(fā)分割
-XX:GCLogFileSize=100M來源:juejin.cn/post/6949885566536138783
干貨分享
最近將個人學習筆記整理成冊,使用PDF分享。關(guān)注我,回復(fù)如下代碼,即可獲得百度盤地址,無套路領(lǐng)??!
?001:《Java并發(fā)與高并發(fā)解決方案》學習筆記;?002:《深入JVM內(nèi)核——原理、診斷與優(yōu)化》學習筆記;?003:《Java面試寶典》?004:《Docker開源書》?005:《Kubernetes開源書》?006:《DDD速成(領(lǐng)域驅(qū)動設(shè)計速成)》?007:全部?008:加技術(shù)群討論
加個關(guān)注不迷路
喜歡就點個"在看"唄^_^
