伺服器上的Java程序總是在執行個兩三天後就無法響應請求了,具體現象如下:
- 請求業務返回狀態碼502,檢視程序還在,意味著Java程序假死,無法響應請求了;
- 該Java程序佔比CPU較高,高達132.8%。
使用top命令檢視伺服器整體執行情況:
可以看到PID為14760的Java程序CPU佔比132.8%,記憶體佔用37.6%,記憶體倒是在合理範圍內,但是這個CPU就太高了。
檢視程序14760的執行緒堆疊資訊
執行命令:
top -Hp 14760
列印出當前程序的所有執行緒的執行情況:
發現PID為14767和14768的兩個執行緒的CPU佔比分別為47.8%和47.5%,而且TIME+佔比較大,說明這兩個執行緒一直在執行。
繼續使用jstack命令來檢視這兩個執行緒的堆疊資訊,jstack的基本命令格式如下:
jstack <pid> | grep '16進位制的執行緒Id'
可以透過如下命令獲取14767這個執行緒的16進位制數值:
printf '%x\n' 14767
輸出:0x39af,執行jstack相關命令:
jstack 14760 | grep -a 0x39af -C20 --color
其中這個-C20是顯示當前行的上下20行 ,如果沒有這個命令就只有孤零零的當前行,看不出更多有效的資訊,輸出內容如下:
透過上圖就可以看到執行緒為什麼吃CPU了,如果顯示的是大家自己開發的業務程式碼,相信大家直接就可以review程式碼找出問題所在了。
但是有時候我們並不能根據執行緒Id就能查出有問題的程式碼在哪裡,比如上圖這種情況,紅框裡的兩個執行緒是GC task Thread,說明執行緒一直在進行GC,而上面的其他執行緒則是出於WAITING等待狀> > 態,也就是說造成Java服務程序假死的原因是因為JVM的長時間GC導致的Stop The World!
所以我們要找出來到底是什麼原因導致的程序一直在GC,而無法響應業務請求。
jstat命令排查GC資訊
既然是GC有問題,那麼我們繼續使用jstat命令來檢視JVM heap的資訊。
先來快速瞭解一下jstat命令的介紹和用法。
jstat命令格式如下:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
相關引數:
- -class:顯示載入class的數量以及所佔空間資訊
- -compiled:顯示VM實時編譯的數量資訊
- -gc:顯示gc的資訊,檢視gc的次數、時間
- -gccapacity:顯示VM記憶體中三代(young、old、perm)物件的使用和佔用大小
- -gcutil:統計gc資訊
- -gcnew:統計VM年輕代資訊
- -gcnewcapacity:VM年輕代物件的資訊及佔用量
- -gcold:VM年老代資訊
- -gcoldcapacity:VM年老代物件的資訊及佔用量
- -gcpermcapacity:VM中永久代的資訊及佔用量
- -printcompilation:當前VM執行的資訊。
執行命令:
jstat -gcutil 14760 1000
得出的GC Heap資料如圖所示:
其中:
- S0:倖存1區當前使用比例
- S1:倖存2區當前使用比例
- E:伊甸園區使用比例
- O:老年代使用比例
- M:後設資料區使用比例
- CCS:壓縮使用比例
- YGC:年輕代垃圾回收次數
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
從圖中的資料可以發現E(Eden區)和O(Old區)的記憶體已經被耗盡了,佔比高達100%,FGC(Full GC)的次數高達23133次,FGCT(Full GC Time)總時間高達36221.931秒,每次FGC耗時36221.931/23133≈1.6秒,很顯然Java程序都把時間花在FGC上了。
jmap命令拿到dump日誌檔案
透過以上資料分析,可以初步定為問題發生在程式的記憶體洩漏上。這時候就需要使用到jmap的命令了,我們可以透過jmap命令輸出指定Java程序的dump二進位制檔案:
jmap -dump:format=b,file=dump.phrof <pid>
接下來開始用MAT工具分析拿到的dump檔案
Eclipse Memory Analyzer是一個快速的並且功能豐富的Java堆分析工具,旨在幫助你發現記憶體洩漏問題和減少記憶體消耗。
使用MAT來分析生產數以億計的物件堆轉儲檔案,快速計算存活物件的大小,發現誰在阻礙GC回收物件,形成一個報告來自動提取記憶體洩漏嫌疑物件。
安裝獨立版本的MAT:https://www.eclipse.org/mat/downloads.php
MAT分析Java Heap Dump檔案
MAT開啟本地dump檔案,選擇Leak Suspects Report:
可以看到
org.hibernate.internal.SessionFactoryImpl這個例項佔用了853,310,632 (92.24%) 位元組,共813.3MB!目前可以知道的是在Hibernate資料查詢這塊有問題,具體點選Details繼續跟進去找問題程式碼:
圖中可以看到有問題的物件好幾個,佔用的記憶體也比較大。再來看下面一張圖:
Accumulated Objects by Class in Dominator Tree是支配樹中按類累積物件,可以看到BoundedConcurrentHashMap只有32個物件示例,其物件樹卻佔據了853241512位元組的堆大小。我們點選進去看看這個HashMap裡面都儲存了什麼資料:
把這個value複製出來,是我們業務的sql,看到這個sql我就驚訝了,這個sql傳了91976個引數!
至此,我們便找出了造成Java程序假死的程式碼在這個SQL上。
毋庸置疑,這個SQL存在兩個大問題:
- SQL本身是慢SQL,不能迅速查出來;
- SQL本身查出來的資料需要分配更多的記憶體。
記憶體不足,需要回收物件,但是查詢SQL有需要更多的物件空間來儲存。因此這邊一直在GC,但是有回收不出來有效的空間,那就需要繼續GC。所以程序一直在FULL GC,導致一直stop the world。
原文章地址:
https://zhuanlan.zhihu.com/p/529350757