1. 程式人生 > >一次線上記憶體溢位的排查

一次線上記憶體溢位的排查

點選上方“程式設計師小灰”,選擇“置頂公眾號”

有趣有內涵的文章第一時間送達!

本文轉載自公眾號 你假笨

或許大家曾經都碰到過HashMap因為其非執行緒安全的多執行緒併發操作導致cpu飆高的問題,不過這個問題在JDK8裡已經解決掉了,其根本原因網上也早已遍地開花,所以我這篇文章裡就不再熬述了,不瞭解的可以去網上找找相關文章,本文和大家聊的是看到的另外一個現象——記憶體溢位。

現象

同事丟了一個連結過來,是記憶體分析的,我看到一個執行緒佔用的記憶體非常高,這個問題其實已然非常明顯了,展開看了下執行緒棧:

640?wx_fmt=png

正在呼叫一個Map物件的toString方法,直到丟擲java.lang.OutOfMemoryError

,之所以這個棧頂能看到OutOfMemoryError的邏輯是因為配置了-XX:+HeapDumpOnOutOfMemoryError引數。

這個引數的官方說明如下:

640?wx_fmt=png

不過這個引數只會生效一次,不會每次OOM的時候都做記憶體dump,大家可以想像一下,如果是程式碼的問題會發生連續的OOM,那連續做dump也沒必要,於是JVM裡控制這個引數只會在第一次發生OOM的時候做一次記憶體dump。

分析

其實在我看到這個OutOfMemoryError棧之後,還沒等同事說多少話,我就立馬要同事去看我之前那篇關於OOM的文章了,想表達的是雖然這個執行緒棧裡看到了OOM,但是記憶體洩露其實不一定是和這個執行緒有關的,可能只是臨門一腳而已,不過後面細看了下這個執行緒佔的記憶體其實真的挺高了,高達2G多,所以就這個案例來說還是和這個執行緒有關的,有時候不能太相信自己的經驗,具體問題還是得具體分析才好。

那為什麼這個執行緒會佔用這麼大的記憶體呢?看到整個棧後面都在做字串的拼接擴容動作,因為都是toString方法觸發的,難道真的有個2G的字串?詢問同事他們說絕對不可能存在這麼大的字串,貌似老早之前有同事問過我類似的問題,不過我都一直懷疑他們說的,覺得肯定是存在這麼大的字串的,只是他們不知道而已,原來那個問題我也已經忘記最後情況了。今天又有類似的問題過來,我想也許我想的真的不對?

後面同事打我電話說了下場景,他列印一個Map,但是這個Map其實是一個ConcurrentHashMap,是執行緒安全的,但是這個map裡的value是一個HashSet,這個HashSet是非執行緒安全的,並且存在多個執行緒修改這個Set的情況,那會不會是因為併發導致的呢,HashSet裡其實就是一個HasMap的結構,我覺得是很有可能的,於是要同事自己去模擬下這個場景,看能否重現出來

我繼續看他們的記憶體dump,果然發現了一些貓膩,確實在列印那個HashSet過程中,next欄位是迴圈連起來的,於是基本確定了死迴圈的存在,沒過一會兒,同事也重現出來了,大概邏輯如下:

640?wx_fmt=png

注意,這個得在JDK6或者7下跑才會重現,JDK8下不存在這個問題

Demo裡就是兩個執行緒同時對HashSet進行修改,可能帶來的一個後果是裡面的HashMap因為要擴容並且做rehash而出現死迴圈的情況,當有執行緒要列印這個HashSet的時候,會呼叫其toString方法,再看看其父類AbstractCollection的toString的邏輯:

640?wx_fmt=png

就是挨個遍歷,然後將值塞到StringBuilder裡,如果正巧之前因為多執行緒的併發操作導致了死迴圈鏈的產生,那可能會導致這個StringBuilder會非常大,並且還會不斷進行擴容,正如上面的堆疊看到的一樣,這直接帶來的一個後果就是出現記憶體溢位。

記憶體富餘下的OutOfMemory

對於同事線上碰到的那個問題看到的OOM提示是Requested array size exceeds VM limit,這個提示講真我還是第一次碰到有發生的,假如說你的記憶體其實非常大,足夠的剩餘,但是當你要建立一個數組的時候,如果你的陣列的長度超過Integer.MAX_VALUE-2的話,那你將會看到一個這個提示的OOM丟擲來,其實這也是你能建立的陣列的最大長度了,這或許很多人都沒有注意到的,就把這個當做本文的一個最有價值的亮點吧。

以下是笨神的個人微信公眾號,

主要圍繞JVM寫一系列的原理性,效能調優的文章

640?wx_fmt=jpeg

喜歡本文的朋友們,歡迎長按下圖關注訂閱號程式設計師小灰,收看更多精彩內容

640?wx_fmt=jpeg