1. 程式人生 > 其它 >《深入理解Java虛擬機器》(八) 記錄一次OOM問題分析實戰

《深入理解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.主要問題現象

  1. GC日誌結合jvisualvm,發現當活動執行緒數量開始飆升,老年代記憶體佔用飆升;
  2. 當堆記憶體空間尚未佔滿時,手動點選垃圾回收,老年代能被回收,不再出現記憶體溢位現象

3.初步分析問題

  • 結合現象推測,某些操作耗時且頻繁,導致老年代擴充速度快於堆記憶體回收速度,最終 記憶體溢位。
  • 處理:使用堆快照分析工具Jprofile查詢問題位置,並處理;

三、相關工具介紹

四、實際問題快照分析

PS * 由於是公司環境,有些日誌就不能提供了

1.通過Memory檢視老年代記憶體佔用情況

觀察老年代曲線,如果該曲線持續攀升,則觀察堆快照。

2.選擇Live Memory 檢視,

  1. 選擇Recorded Objects 觀察堆記憶體活物件

  2. 點選Mark Current Values記錄某一時刻存活的所有物件,並與實時存活兌現數量對比

  3. 根據size關鍵字,找到堆內佔據最多記憶體空間的物件,或者多次GC操作後,例項數
    不減反增的物件;

3.生成堆快照,分析物件資訊

  1. 以String為例,當前快照對比上一步記錄的物件例項數,增加了36767個String類物件

  2. Use -> selected instance,根據如圖方式追蹤物件的產生

  3. 選擇Allocations 檢視追蹤相關方法執行堆疊資訊

最終分析該方法程式碼,定位問題。

五、程式碼邏輯問題

經過分析堆快照,發現問題:

  1. 系統裡有一個提供給第三方終端裝置使用的介面效能較差,查詢非常耗時,且對方呼叫頻率過於頻繁,最終通過使用快取技術解決了這個問題。

  2. 經過排查發現一個歷史程式碼遺留的BUG,某些情景下方法入參為空時,導致了一個SQL查整表。

六、效能問題

1.問題現象

處理完上述問題後,專案上又一次反饋系統卡頓;如圖:記憶體佔用較低,不存在記憶體即將溢位問題,經過排查,堆內也未發現異常物件

然後,留意到垃圾回收曲線已經變成波浪線,再次檢視GC日誌發現:

  1. eden區滿,頻繁新生代GC
  2. 新生代共計不足500M記憶體佔用,回收耗時超過一秒
  3. 垃圾回收過程中Ref Proc,處理各種引用耗時近似垃圾回收耗時

    (模擬日誌,體現Ref Proc耗時情況)

2.問題處理方法

(G1垃圾收集器)

	-XX:+ParallelRefProcEnabled 開啟並行處理各種引用    
	-XX:ParallelGCThreads=8 並行垃圾回收處理執行緒數
	-XX:ConcGCThreads=3 併發標記執行緒數 ,約等於 ParallelGCThreads / 4 

假設,cpu核心數 = X

3.處理結果

  1. 處理後GC曲線:

  2. JVM執行狀況:
    平均15分鐘內2~3 次新生代GC,每次GC停頓時間低於10 毫秒