前言
對於後端程式設計師,特別是 Java 程式設計師來講,排查線上問題是不可避免的。各種 CPU 飈高,記憶體溢位,頻繁 GC 等等,這些都是令人頭疼的問題。樓主同樣也遇到過這些問題,那麼,遇到這些問題該如何解決呢?首先,出現問題,肯定要先定位問題所在,然後分析問題原因,再然後解決問題,最後進行總結,防止下次再次出現。
1. CPU 飈高
線上 CPU 飈高問題大家應該都遇到過,那麼如何定位問題呢?
思路:首先找到 CPU 飈高的那個 Java 程式,因為你的伺服器會有多個 JVM 程式。然後找到那個程式中的 “問題執行緒”,最後根據執行緒堆疊資訊找到問題程式碼。最後對程式碼進行排查。
如何操作呢?
通過 top 命令找到 CPU 消耗最高的程式,並記住程式 ID。 再次通過 top -Hp [程式 ID] 找到 CPU 消耗最高的執行緒 ID,並記住執行緒 ID. 通過 JDK 提供的 jstack 工具 dump 執行緒堆疊資訊到指定檔案中。具體命令:jstack -l [程式 ID] >jstack.log。 由於剛剛的執行緒 ID 是十進位制的,而堆疊資訊中的執行緒 ID 是 16 進位制的,因此我們需要將 10 進位制的轉換成 16 進位制的,並用這個執行緒 ID 在堆疊中查詢。使用 printf "%x\n" [十進位制數字] ,可以將 10 進位制轉換成 16 進位制。 通過剛剛轉換的 16 進位制數字從堆疊資訊裡找到對應的執行緒堆疊。就可以從該堆疊中看出端倪。 從樓主的經驗來看,一般是某個業務死迴圈沒有出口,這種情況可以根據業務進行修復。還有 C2 編譯器執行編譯時也會搶佔 CPU,什麼是 C2 編譯器呢?當 Java 某一段程式碼執行次數超過 10000 次(預設)後,就會將該段程式碼從解釋執行改為編譯執行,也就是編譯成機器碼以提高速度。而這個 C2 編譯器就是做這個的。如何解決呢?專案上線後,可以先通過壓測工具進行預熱,這樣,等使用者真正訪問的時候,C2 編譯器就不會干擾應用程式了。如果是 GC 執行緒導致的,那麼極有可能是 Full GC ,那麼就要進行 GC 的優化。
2. 記憶體問題排查
說完了 CPU 的問題排查,再說說記憶體的排查,通常,記憶體的問題就是 GC 的問題,因為 Java 的記憶體由 GC 管理。有 2 種情況,一種是記憶體溢位了,一種是記憶體沒有溢位,但 GC 不健康。
記憶體溢位的情況可以通過加上 -XX:+HeapDumpOnOutOfMemoryError 引數,該引數作用是:在程式記憶體溢位時輸出 dump 檔案。
有了 dump 檔案,就可以通過 dump 分析工具進行分析了,比如常用的 MAT,Jprofile,jvisualvm 等工具都可以分析,這些工具都能夠看出到底是哪裡溢位,哪裡建立了大量的物件等等資訊。
第二種情況就比較複雜了。GC 的健康問題。
通常一個健康的 GC 是什麼狀態呢?根據樓主的經驗,YGC 5 秒一次左右,每次不超過 50 毫秒,FGC 最好沒有,CMS GC 一天一次左右。
而 GC 的優化有 2 個維度,一是頻率,二是時長。
我們看 YGC,首先看頻率,如果 YGC 超過 5 秒一次,甚至更長,說明系統記憶體過大,應該縮小容量,如果頻率很高,說明 Eden 區過小,可以將 Eden 區增大,但整個新生代的容量應該在堆的 30% - 40% 之間,eden,from 和 to 的比例應該在 8:1:1 左右,這個比例可根據物件晉升的大小進行調整。
如果 YGC 時間過長呢?YGC 有 2 個過程,一個是掃描,一個是複製,通常掃描速度很快,複製速度相比而言要慢一些,如果每次都有大量物件要複製,就會將 STW 時間延長,還有一個情況就是 StringTable ,這個資料結構中儲存著 String.intern 方法返回的常連池的引用,YGC 每次都會掃描這個資料結構(HashTable),如果這個資料結構很大,且沒有經過 FGC,那麼也會拉長 STW 時長,還有一種情況就是作業系統的虛擬記憶體,當 GC 時正巧作業系統正在交換記憶體,也會拉長 STW 時長。
再來看看 FGC,實際上,FGC 我們只能優化頻率,無法優化時長,因為這個時長無法控制。如何優化頻率呢?
首先,FGC 的原因有幾個,1 是 Old 區記憶體不夠,2 是後設資料區記憶體不夠,3 是 System.gc(), 4 是 jmap 或者 jcmd,5 是 CMS Promotion failed 或者 concurrent mode failure,6 JVM 基於悲觀策略認為這次 YGC 後 Old 區無法容納晉升的物件,因此取消 YGC,提前 FGC。
通常優化的點是 Old 區記憶體不夠導致 FGC。如果 FGC 後還有大量物件,說明 Old 區過小,應該擴大 Old 區,如果 FGC 後效果很好,說明 Old 區存在了大量短命的物件,優化的點應該是讓這些物件在新生代就被 YGC 掉,通常的做法是增大新生代,如果有大而短命的物件,通過引數設定物件的大小,不要讓這些物件進入 Old 區,還需要檢查晉升年齡是否過小。如果 YGC 後,有大量物件因為無法進入 Survivor 區從而提前晉升,這時應該增大 Survivor 區,但不宜太大。
上面說的都是優化的思路,我們也需要一些工具知道 GC 的狀況。
JDK 提供了很多的工具,比如 jmap ,jcmd 等,oracle 官方推薦使用 jcmd 代替 jmap,因為 jcmd 確實能代替 jmap 很多功能。jmap 可以列印物件的分佈資訊,可以 dump 檔案,注意,jmap 和 jcmd dump 檔案的時候會觸發 FGC ,使用的時候注意場景。
還有一個比較常用的工具是 jstat,該工具可以檢視 GC 的詳細資訊,比如 eden ,from,to,old 等區域的記憶體使用情況。
還有一個工具是 jinfo,該工具可以檢視當前 jvm 使用了哪些引數,並且也可以在不停機的情況下修改引數。
包括我們上面說的一些分析 dump 檔案的視覺化工具,MAT,Jprofile,jvisualvm 等,這些工具可以分析 jmap dump 下來的檔案,看看哪個物件使用的記憶體較多,通常是能夠查出問題的。
還有很重要的一點就是,線上環境一定要帶上 GC 日誌!!!
總結 基於文章的標題,我們這個是基本操作,故障排查是說不完的話題,每個故障涉及的知識也都很多,因此,我們在學習了基本的排查之後,還需要學習更多事故排查技術,比如排查 IO,網路,TCP 連線等等。
【java後端技術精選】 專注Java技術乾貨:SSM、Spring全家桶、Spring Boot、Spring Cloud、Intellij IDEA、Dubbo、Zookeeper、Redis、git、微服務、MySQL、叢集、分散式、中介軟體、訊息佇列、Linux、網路、多執行緒,運維Jenkins、Nexus、Docker、ELK、公眾號開發,小程式開發、偶爾分享些技術乾貨,面試題,致力於Java全棧開發 !