java執行反射代碼導致頻繁創建奇怪的類從而發生元數據區被佔滿FUllGc案例

查看 JVM GC 日誌

沒有一絲耽擱,老王立刻調出了線上GC日誌,在日誌裏,看到了一個“Metadata GC Threshold”的字樣,類似於如下日誌:

【Full GC(Metadata GC Threshold)xxxxx, xxxxx】

通過GC日誌,老王立即定位到這次頻繁的Full GC,實際上是JDK 1.8以後的Metadata元數據區導致的。

奇怪了,Metadata區域一般是放一些加載到JVM裏去的類,爲什麼會被頻繁的塞滿,進而觸發Full GC呢?

老王陷入了沉思,一旁的小猛和彪子只是靜靜的看着,大氣也不敢出。。。

查看 Metaspace 內存佔用情況

對啊,怎麼把這茬給忘了”。老王突然的自言自語,把兩個小弟嚇了一跳。

思考中的老王根本沒注意到兩個小弟的反應,他迅速通過jstat,觀察了一下Metaspace內存區域佔用的波動曲線圖,發現圖像類似下面這樣:
在這裏插入圖片描述
看起來Metaspace區域的內存呈現一個波動的狀態,總是會先不斷增加,達到一個頂點之後,就會把Metaspace區域給佔滿

佔滿之後,自然就會觸發一次Full GC,Full GC會帶着Metaspace區域的垃圾回收,所以接下來Metaspace區域的內存佔用又變得很小了。

看着屏幕上的圖像,老王心裏一絲明悟。很明顯系統在運行過程中,不停的有新的類產生被加載到Metaspace區域裏,然後不停的把Metaspace區域佔滿。接着觸發一次Full GC,回收掉Metaspace區域中的部分類。

然後這個過程不斷循環,進而造成Metaspace區域反覆被佔滿,然後反覆導致Full GC的發生。

老王一邊思考,一邊在紙上畫出了一張示意圖:
在這裏插入圖片描述

到底是什麼類在不停的加載?

那問題又來了,到底是什麼類不停的被加載到JVM的Metaspace區域裏去?老王又一次陷入了沉思。

不多會,他回過神來,在JVM啓動參數中加入如下兩個參數了:

-XX:TraceClassLoading 
-XX:TraceClassUnloading

“老大,這兩個參數有什麼作用啊?”彪子忍不住好奇問道。

“這兩個參數,顧名思義,就是追蹤類加載和類卸載的情況,他會通過日誌打印出來JVM中加載了哪些類,卸載了哪些類”,老王一邊緊盯屏幕,一邊解釋道。

果然,加入這兩個參數之後,在Tomcat的catalina.out日誌文件中輸出了一堆日誌,裏面顯示類似如下的內容:

【Loaded sun.reflect.GeneratedSerializationConstructorAccessor from __JVM_Defined_Class】

明顯可以看到,JVM在運行期間不停的加載了大量的名字叫做“GeneratedSerializationConstructorAccessor”類到Metaspace區域裏去
看着這個莫名其妙的類,老王在之前的草圖上又補充了幾筆:
在這裏插入圖片描述
雖然不知道這是什麼類,但是老王相信就是它的一直加載,導致 Metaspace 區域不停被佔滿,進而不停的引發Full GC

直覺告訴他,已經慢慢接近真相了!

爲什麼會頻繁加載奇怪的類?

“彪子,你馬上百度一下這個類是什麼?”老王指着剛纔日誌追蹤到的這個奇怪的類名說道。

“好的好的”,彪子哪敢怠慢,三下五除二就百度了出來

“老大,這個類是Java執行反射機制時加載出的一個類,JVM會在反射調用一定次數之後動態生成一些類,剛纔這個類就是其中之一,這個是JVM的一個底層優化機制”

彪子唸完上面一段話,一臉懵逼,自己都不知道自己在說啥。。。

不停執行反射代碼後動態生成的類。。。老王陷入沉思,邊思考邊在紙上又畫出了一張圖:
在這裏插入圖片描述

JVM 創建的奇怪類的玄機

經過短暫的思考後,老王一絲恍悟。想到了自己在公衆號儒猿技術窩的專欄《從零開始帶你成爲JVM實戰高手》裏學過的如下知識:

JVM在反射過程中動態生成的類的Class對象,他們都是SoftReference軟引用的,而軟引用正常情況下不會回收,但是如果內存比較緊張的時候就會回收這些對象。

那SoftReference對象到底在GC的時候要不要回收,是怎麼判斷的呢?老王大腦繼續飛速運轉

對了,就是這個!老王一拍大腿,在紙上迅速寫下了一個等式。

clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB

看着老大寫出的這個公式,兩個小弟的反應完全不同。小猛一臉懵逼,而彪子則一臉淡然。

彪子看到好哥們小猛的反應,主動給他解釋道:“clock - timestamp”代表了一個軟引用對象有多久沒被訪問過了。freespace代表JVM中的空閒內存空間,SoftRefLRUPolicyMSPerMB代表每一MB空閒內存空間可以允許SoftReference對象存活多久。”

“舉個例子,假如現在JVM創建了一大堆奇怪的類出來,這些類本身的Class對象都是被SoftReference軟引用的。”

“然後現在JVM裏的內存空間有3000MB,SoftRefLRUPolicyMSPerMB的默認值是1000毫秒,那麼就意味着,此時那些奇怪的SoftReference軟引用的Class對象,可以存活3000 * 1000 = 3000秒,就是50分鐘左右。”

“是的,小彪說的沒錯”,老王一邊思考,一邊評價着彪子剛纔的一番解答

得到了老大的認可,彪子心裏一陣小得意,而小猛則是一臉不可思議,心想:“這貨跟我合租一套房,白天一塊上班,晚上一塊開黑,這些東西哪裏看的”

其實他不知道的是,彪子游戲玩的很渣,每局都早早的掛掉了,所以在等待下一局的時候,就會無聊逛逛技術博客啥的

剛纔說的這些,就是在他等待玩下一把遊戲之前的空隙,無意中get到的

但是,正是因爲彪子get到的這個他引以爲傲的“黑科技”,導致了今天這次線上事故。

怎麼回事兒呢?我們接着看!

話說老王在肯定了彪子對自己寫下的這個等式的解釋後,又繼續說道:

“當然小彪上面那個例子,僅僅是舉例而已。一般來說,發生GC時,其實JVM內部或多或少總有一些空間內存的,所以基本上如果不是快要發生OOM內存溢出了,一般軟引用也不會被回收。”

“所以按理說,JVM應該會隨着反射代碼的執行,動態的創建一些奇怪的類,他們的Class對象都是軟引用的,正常情況下不會被回收,也不應該快速增長。”

問題解決,水落石出

分析到這裏,老王感覺自己離解決方案就差一層窗戶紙,一捅就破。

眼睛緊緊盯着紙上的公式,他忽然想到了什麼,打開了線上的JVM參數設置,找到剛纔公式裏的參數:SoftRefLRUPolicyMSPerMB

“果然是它”,老王自言自語道,隨後他快速敲擊着鍵盤,重新設置了JVM參數,重啓之後,系統穩定運行了

“神了。。。老大,你怎麼解決的?”小猛和彪子一臉的崇拜和不可思議。

老王並沒有理會兩個小弟的問題,反問道:“這個SoftRefLRUPolicyMSPerMB參數,是誰設置爲0的?”

“額。。。是我設置的!”彪子額頭上頓時冷汗岑岑。

不過他馬上補充道:“是我沒事的時候在網上逛技術博客,偶然看到了關於這個JVM參數的講解,以及軟引用什麼時候被JVM垃圾回收的判斷依據”。

說到這裏,彪子指了指老王寫在紙上的公式,“就是老大你寫的這個等式”。

“然後我想着把SoftRefLRUPolicyMSPerMB這個參數設置爲0,任何軟引用對象就可以儘快釋放掉,不用留存,儘量給內存釋放空間出來,這樣就可以提高內存利用效率了!”

老王聽完一陣苦笑,緩緩的吐出5個字:“很傻很天真。。。”
稍微頓了頓,他給彪子解釋了起來:“實際上一旦這個參數設置爲0之後,直接導致clock-timestamp<=freespace*SoftRefLRUPolicyMSPerMB這個公式的右半邊是0,就導致所有的軟引用對象,比如JVM生成的那些奇怪的Class對象,剛創建出來就可能被一次Young GC給帶着立馬回收掉一些。”

邊說老王還邊給彪子畫了一張圖:
在這裏插入圖片描述
“舉個例子,比如JVM好不容易給你弄出來100個這種奇怪的類,結果因爲你瞎設置軟引用的參數,導致突然一次GC就給你回收掉幾十個類”

“接着JVM在反射代碼執行的過程中,就會繼續創建這種奇怪的類,在JVM的機制之下,會導致這種奇怪類越來越多。”

“也許下一次gc又會回收掉一些奇怪的類,但是馬上JVM還會繼續生成這種類,最終就會導致Metaspace區域被放滿了,一旦Metaspace區域被佔滿了,就會觸發Full GC,然後回收掉很多類,接着再次重複上述循環”

老王一邊說,一邊又畫出了一張圖:

在這裏插入圖片描述
老王緊接着繼續說道:“其實解決方案很簡單,在有大量反射代碼的場景下,只要把-XX:SoftRefLRUPolicyMSPerMB這個參數設置大一些即可,千萬別設置爲0,可以設置個1000,2000,3000,或者5000毫秒,都可以。”

“提高這個數值,就是讓反射過程中JVM自動創建的軟引用的一些類的Class對象不要被隨便回收,你看我優化這個參數之後,系統是不是就穩定運行了,然後Metaspace區域的內存佔用也是穩定的,不會來回大幅度波動了”

兩個小弟聽得入了迷。。。

還是小猛先回過神來:“老大,雖然最後解決方案看起來很簡單,但是我覺得整個過程中的問題定位和分析纔是重點,可以教教我嗎?”小猛一臉的求知若渴

“是的是的”,彪子趕緊附和着說道。

老王似乎並沒有打算回答兩個小弟的問題,相反他臉上顯現出一絲小小的失望。

嘆了口氣,他反問小猛和彪子:“這個月1號,我給你們佈置過什麼任務?”

看了看日子,1號是週五,小猛和彪子面面相覷。唯一記得的,就是那天他倆下班後,一起吃了個飯,然後就相約回出租屋裏,開黑玩王者,一直到深夜!

見兩個小弟陷入沉思,老王打破沉默:“我是不是給你們推薦了一個專欄《從零開始帶你成爲JVM實戰高手》,並且交待你們用幾周的時間,好好消化裏面的內容!

頓了頓,老王又繼續說道:“今天能這麼迅速的解決這個線上事故,不是我多神,而是因爲這個案例正是專欄裏講的一個案例。

我把整個專欄反覆閱讀了3遍,甚至可以告訴你們,這個案例在專欄的第59講”

小猛和彪子聞言大驚,紛紛掏出手機,退出之前還在的王者榮耀界面,進入公衆號儒猿技術窩的知識店鋪,翻到第59講,標題赫然在目:

在這裏插入圖片描述
老王繼續說道:“不論是平時工作,還是跳槽面試,JVM一直是咱們Java程序員的一大痛點。以前我自己也看過不少JVM書籍,理論知識掌握了不少,但是對於線上實戰和性能優化,總感覺心裏很虛”

而《從零開始帶你成爲JVM實戰高手》專欄,可以說填補了國內 JVM 實戰這塊技術資料的空白,它深入講解了下面三塊實戰內容:

1.線上系統的JVM參數優化實戰:基於百萬交易量的支付系統、上億用戶量的電商系統實戰

2.線上系統的JVM GC調優實戰:基於每秒10萬併發的BI系統、日百億數據量的數據處理系統進行實戰

3.線上系統的OOM內存溢出優化實戰:基於百萬連接網關係統、10萬併發量的秒殺系統,進行實戰

他在專欄裏詳細剖析了自己親身經歷的34個真實線上JVM生產案例,非常適用於我們這種中小型的創業公司

除了實戰,專欄也有非常詳細的對JVM原理性知識的講解,而且寫作方式很接地氣,一步一圖,循序漸進、通俗易懂

你們看看這個專欄大綱,也能體會到作者設計大綱的用心良苦了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章