線上服務Java程序假死快速排查、分析

快乐小洋人發表於2024-04-09

伺服器上的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

相關文章