查看 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原理性知識的講解,而且寫作方式很接地氣,一步一圖,循序漸進、通俗易懂
你們看看這個專欄大綱,也能體會到作者設計大綱的用心良苦了