《深入理解Java虛擬機器》(八) 記錄一次OOM問題分析實戰
一、問題分析思路
1.考慮一個問題現象,系統剛啟動在訪問量比較小的時候執行流暢,隨著訪問量提高,開始卡頓;
2.可以先排查資料庫問題,例如Oracle可以檢查臨時表空間,慢sql統計,索引等等因素,如果依然無法解決;
3.最終再考慮JVM調優,正常情況下是不需要JVM調優的,個人以為,JVM調優更多是針對比較極端的情景的。
4.為什麼會系統卡頓? 結合個人見過的問題,一般都是堆上發生的GC,導致系統停頓時間過長,或者過於頻繁進行GC,它們都會造成效能損耗,不用猜,此時一般都是在OOM問題的邊緣來回試探了。
5.OOM問題表現:
記憶體洩露
比如出現異常或者程式碼bug,某些物件產生後就不能被回收。已知:一般情況下,物件在新生代上分配,如果多次收集後依然存活則進入老年代(大物件會直接進入老年代)。不能被回收的物件,會不斷佔用老年代空間 最終,老年代記憶體空間佔滿,新生代物件需要晉升時無法請求到足夠記憶體空間,進入Full GC環節。
效能較差程式碼導致大量物件無法被快速回收
該問題表現為,在堆記憶體空間還有剩餘時,假設堆上限8G,只要還剩餘一定空間時,例如:幾百M、或者1G,手動觸發Full GC,老年代就會被回收掉,但是放任不管,最終肯定會因為物件分配速度過快導致堆記憶體佔滿。
PS *
- Full GC (STW操作,所有使用者執行緒停頓)
- Minor GC/Young GC–目標是新生代的垃圾收集,新生代空間不足時觸發,新生代中物件根據存活次數判定是否晉升
- Major GC/Old GC–目標是老年代的垃圾收集
- Mixed GC --目標是整個新生代和部分老年代的垃圾收集(只有G1垃圾收集器存在該行為)。
其實很好理解:Full GC是較難觸發的(可以自行了解Full GC觸發條件),一般都是較為極端的情況才會觸發Full GC;
區別是:
- full gc頻率極低,且full gc 能回收掉大量資源,那麼你是個好人,你可以走了;
- full gc頻率高,並且不能有效回收記憶體空間,那麼可以證明你不對勁,要把你抓起來。一般會指向OOM問題
理解為:我大意了,我沒有閃,直到最後知道Full GC到來的我眼淚掉下來。。。。。
PS* 此時或許確實有許多物件可以被回收,但是瞭解回收演算法的人都應該需要知道:
- G1的標記整理演算法,需要額外有剩餘空間才能進行物件的整理,如果堆空間佔滿了,那麼能整理到哪裡去呢?此時Full GC就代表敗北,你儘管去進行Full GC,能成功算我輸。。。
- CMS收集器, 標記-清除演算法,碎片化問題,如果無法找到足夠的連續記憶體空間分配某個物件,那麼就會一直進行full GC。。。。
- 除了這倆,應該不會有人再用更古老的收集器了吧?如果有可以看我另一篇部落格: 深入理解Java虛擬機器》(二) GC 垃圾回收機制
有如下對話為證
- 堆:我真的一滴都沒有了
- 但是分配記憶體的請求就像磨人的小妖精表示:我不管,你給我去擠,你去找
- 於是乎,堆上就會一遍又一遍進行垃圾回收以期待能擠出那麼一滴記憶體空間
- 但是此時堆的體量非常大,每次回收計算引用關係、物件存活資訊會非常耗時
- 表現為,系統時時刻刻都會進行非常耗時full gc (stop the work!!!)
- 到了最後絕望的死在了xx的肚皮上
二、主要問題概述以及分析
上班高峰期間,系統使用一段時間後崩潰,報錯記憶體溢位;
1.相關操作
列印系統執行GC日誌,初步分析堆記憶體分代情況;jvisualvm實時觀察系統執行過程中堆記憶體佔用、活動執行緒數量曲線。
2.主要問題現象
- GC日誌結合jvisualvm,發現當活動執行緒數量開始飆升,老年代記憶體佔用飆升;
- 當堆記憶體空間尚未佔滿時,手動點選垃圾回收,老年代能被回收,不再出現記憶體溢位現象
3.初步分析問題
- 結合現象推測,某些操作耗時且頻繁,導致老年代擴充速度快於堆記憶體回收速度,最終 記憶體溢位。
- 處理:使用堆快照分析工具Jprofile查詢問題位置,並處理;
三、相關工具介紹
-
執行環境:JDK 1.8
-
垃圾收集器:G1
-
JvisualVM, JDK自帶分析工具,對執行中的程式影響較小,可以實時監控5
-
JProfile簡介,
JProfile詳細簡介可以看我轉載的這篇文章:
https://blog.csdn.net/bokerr/article/details/114445489
四、實際問題快照分析
PS * 由於是公司環境,有些日誌就不能提供了
1.通過Memory檢視老年代記憶體佔用情況
觀察老年代曲線,如果該曲線持續攀升,則觀察堆快照。
2.選擇Live Memory 檢視,
-
選擇Recorded Objects 觀察堆記憶體活物件
-
點選Mark Current Values記錄某一時刻存活的所有物件,並與實時存活兌現數量對比
-
根據size關鍵字,找到堆內佔據最多記憶體空間的物件,或者多次GC操作後,例項數
不減反增的物件;
3.生成堆快照,分析物件資訊
-
以String為例,當前快照對比上一步記錄的物件例項數,增加了36767個String類物件
-
Use -> selected instance,根據如圖方式追蹤物件的產生
-
選擇Allocations 檢視追蹤相關方法執行堆疊資訊
最終分析該方法程式碼,定位問題。
五、程式碼邏輯問題
經過分析堆快照,發現問題:
-
系統裡有一個提供給第三方終端裝置使用的介面效能較差,查詢非常耗時,且對方呼叫頻率過於頻繁,最終通過使用快取技術解決了這個問題。
-
經過排查發現一個歷史程式碼遺留的BUG,某些情景下方法入參為空時,導致了一個SQL查整表。
六、效能問題
1.問題現象
處理完上述問題後,專案上又一次反饋系統卡頓;如圖:記憶體佔用較低,不存在記憶體即將溢位問題,經過排查,堆內也未發現異常物件
然後,留意到垃圾回收曲線已經變成波浪線,再次檢視GC日誌發現:
- eden區滿,頻繁新生代GC
- 新生代共計不足500M記憶體佔用,回收耗時超過一秒
- 垃圾回收過程中Ref Proc,處理各種引用耗時近似垃圾回收耗時
(模擬日誌,體現Ref Proc耗時情況)
2.問題處理方法
(G1垃圾收集器)
-XX:+ParallelRefProcEnabled 開啟並行處理各種引用
-XX:ParallelGCThreads=8 並行垃圾回收處理執行緒數
-XX:ConcGCThreads=3 併發標記執行緒數 ,約等於 ParallelGCThreads / 4
假設,cpu核心數 = X
3.處理結果
-
處理後GC曲線:
-
JVM執行狀況:
平均15分鐘內2~3 次新生代GC,每次GC停頓時間低於10 毫秒