新聞中心
記一次隱藏很深的 JVM 線上慘案的分析、排查、解決
作者:Java的小本家 2019-09-10 10:31:10
云計算
虛擬化 本文會給大家講解一個比較特殊的JVM優(yōu)化案例,這個優(yōu)化案例本身是因為新手工程師對JVM優(yōu)化可能了解了一個半吊子,然后不知道從哪里找來了一個非常特殊的JVM參數(shù)錯誤的設(shè)置了一下,就導(dǎo)致線上系統(tǒng)頻繁的出現(xiàn)Full GC的問題。

十多年的揚州網(wǎng)站建設(shè)經(jīng)驗,針對設(shè)計、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。營銷型網(wǎng)站建設(shè)的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整揚州建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計,從而大程度地提升瀏覽體驗。創(chuàng)新互聯(lián)公司從事“揚州網(wǎng)站設(shè)計”,“揚州網(wǎng)站推廣”以來,每個客戶項目都認(rèn)真落實執(zhí)行。
1、本文背景
本文會給大家講解一個比較特殊的JVM優(yōu)化案例,這個優(yōu)化案例本身是因為新手工程師對JVM優(yōu)化可能了解了一個半吊子,然后不知道從哪里找來了一個非常特殊的JVM參數(shù)錯誤的設(shè)置了一下,就導(dǎo)致線上系統(tǒng)頻繁的出現(xiàn)Full GC的問題。
但是我們后續(xù)大量的優(yōu)化案例其實都是各種各樣奇形怪狀的場景,因為正是各種奇怪場景才能讓大家逐步積累出來較為豐富的JVM優(yōu)化實戰(zhàn)經(jīng)驗
了解的場景越多,自己未來在處理JVM性能問題的時候才能更是得心應(yīng)手。
2、問題的產(chǎn)生
這個場景的發(fā)生大致如下過程:某天團隊里一個新手工程師大概是心血來潮,覺得自己網(wǎng)上看到了某個JVM參數(shù),以為學(xué)會了絕世武功秘籍,于是就在當(dāng)天上線一個系統(tǒng)的時候,自作主張設(shè)置了一個JVM參數(shù)
這個參數(shù)是什么呢?
不用急,跟著看下面的案例分析即可,現(xiàn)在只要知道他設(shè)置了一個奇怪的參數(shù),接著事故就發(fā)生了。
因為一般中大型公司都是接入類似Zabbix、OpenFalcon或者公司自研的一些監(jiān)控系統(tǒng)的,監(jiān)控系統(tǒng)一般都做的很好,可以讓你的系統(tǒng)直接接入進(jìn)去,然后在上面可以看到每臺機器的CPU、磁盤、內(nèi)存、網(wǎng)絡(luò)的一些負(fù)載。
而且可以看到你的JVM的內(nèi)存使用波動折線圖,還有你的JVM GC發(fā)生的頻率折線圖。包括如果你自己上報某個業(yè)務(wù)指標(biāo),也可以在監(jiān)控系統(tǒng)里看到。
而且一般都會針對線上運行的機器和系統(tǒng)設(shè)置一些報警,比如說,你可以設(shè)置如果10分鐘內(nèi)發(fā)現(xiàn)一個系統(tǒng)的JVM發(fā)生了超過3次Full GC,就必須發(fā)送報警給你,可以發(fā)送給你的短信、郵箱或者是釘釘之類的IM工具。
類似這樣的監(jiān)控系統(tǒng)不在我們的專欄范疇內(nèi),建議大家自己可以去查閱資料,其實基于我們講解的命令行工具,比如jstat,你可以通過linux上的一些命令,讓jstat自動對jvm進(jìn)行監(jiān)控,把監(jiān)控結(jié)果可以輸出到機器的某個文件里去。
然后第二天你就可以去查閱那個文件,也可以看到那臺機器的jvm的一些gc統(tǒng)計。
所以說,沒有可視化工具,用最簡單的命令行工具,其實同樣可以起到類似的效果。
所以那天那個工程師設(shè)置了一個JVM參數(shù)之后,直接導(dǎo)致線上頻繁接到JVM的Full GC的報警,大家就很奇怪了,于是就開始排查那個系統(tǒng)了。
3、查看GC日志
之前已經(jīng)給大家講解過如何在啟動系統(tǒng)的時候讓他輸出GC日志,所以一旦發(fā)現(xiàn)報警,直接登錄到線上機器,然后就看到對應(yīng)的GC日志了。
此時我們看到在GC日志中有大量的Full GC的記錄。
那么是為什么導(dǎo)致的Full GC呢?
在日志里,看到了一個“Metadata GC Threshold”的字樣,類似于如下日志:
【Full GC(Metadata GC Threshold)xxxxx, xxxxx】
從這里就知道,這頻繁的Full GC,實際上是JDK 1.8以后的Metadata元數(shù)據(jù)區(qū)導(dǎo)致的,也就是類似我們之前說的永久代。
這個Metadata區(qū)域一般是放一些加載到JVM里去的類的。
所以此時就很奇怪了,為什么會因為Metadata區(qū)域頻繁的被塞滿,進(jìn)而觸發(fā)Full GC?而且Full GC大家都知道,會帶動CMS回收老年代,還會回收Metadata區(qū)域本身。
我們先看看下圖:
4、查看Metaspace內(nèi)存占用情況
接著我們當(dāng)然是想看一看Metaspace區(qū)域的內(nèi)存占用情況了,簡單點你可以通過jstat來觀察,如果有監(jiān)控系統(tǒng),他會給你展示出來一個Metaspace內(nèi)存區(qū)域占用的波動曲線圖,類似下面這種。
看起來Metaspace區(qū)域的內(nèi)存呈現(xiàn)一個波動的狀態(tài),他總是會先不斷增加,達(dá)到一個頂點之后,就會把Metaspace區(qū)域給占滿,然后自然就會觸發(fā)一次Full GC,F(xiàn)ull GC會帶著Metaspace區(qū)域的垃圾回收,所以接下來Metaspace區(qū)域的內(nèi)存占用又變得很小了。
5、一個綜合性的分析思路
看到這里,相信大家肯定有一點感覺了,這個很明顯是系統(tǒng)在運行過程中,不停的有新的類產(chǎn)生被加載到Metaspace區(qū)域里去,然后不停的把Metaspace區(qū)域占滿,接著觸發(fā)一次Full GC回收掉Metaspace區(qū)域中的部分類。
然后這個過程反復(fù)的不斷的循環(huán),進(jìn)而造成Metaspace區(qū)域反復(fù)被占滿,然后反復(fù)導(dǎo)致Full GC的發(fā)生,如下圖所示。
6、到底是什么類不停的被加載?
接著我們就有點奇怪了,到底是什么類不停的被加載到JVM的Metaspace區(qū)域里去?
這個時候就需要在JVM啟動參數(shù)中加入如下兩個參數(shù)了:
- “-XX:TraceClassLoading -XX:TraceClassUnloading”
這兩個參數(shù),顧名思義,就是追蹤類加載和類卸載的情況,他會通過日志打印出來JVM中加載了哪些類,卸載了哪些類。
加入這兩個參數(shù)之后,我們就可以看到在Tomcat的catalina.out日志文件中,輸出了一堆日志,里面顯示類似如下的內(nèi)容:
【Loaded sun.reflect.GeneratedSerializationConstructorAccessor from __JVM_Defined_Class】
明顯可以看到,JVM在運行期間不停的加載了大量的所謂“GeneratedSerializationConstructorAccessor”類到了Metaspace區(qū)域里去
如下圖所示
相信就是因為JVM運行期間不停的加載這種奇怪的類,然后不停的把Metaspace區(qū)域占滿,才會引發(fā)不停的執(zhí)行Full GC的。
這是一個非常實用的技巧,各位同學(xué)一定要掌握,頻繁Full GC不光是老年代觸發(fā)的,有時候也會因為Metaspace區(qū)域的類太多而觸發(fā)。
到此為止,已經(jīng)慢慢接近真相了。
7、為什么會頻繁加載奇怪的類?
接著遇到類似這種問題,我們就應(yīng)該找一下Google或者是百度了,當(dāng)然推薦是用Google。你完全可以看看那種不停加載的類,到底是什么類,是你自己寫的類?還是說JDK內(nèi)置的類?
比如上面的那個類,如果你查閱一些資料,很容易就會搞明白,那個類大概是在你使用Java中的反射時加載的,所謂反射代碼類似如下所示。
- Method method = XXX.class.getDeclaredMethod(xx,xx);
- method.invoke(target,params);
友情提示一下,反射是Java中最最基礎(chǔ)的一個概念,不懂的朋友自己查一下資料。
簡單來說,就是通過XXX.class獲取到某個類,然后通過geteDeclaredMethod獲取到那個類的方法。
這個方法就是一個Method對象,接著通過Method.invoke可以去調(diào)用那個類的某個對象的方法,大概就這個意思。
在執(zhí)行這種反射代碼時,JVM會在你反射調(diào)用一定次數(shù)之后就動態(tài)生成一些類,就是我們之前看到的那種莫名其妙的類
下次你再執(zhí)行反射的時候,就是直接調(diào)用這些類的方法,這是JVM的一個底層優(yōu)化的機制。
看到這里,有的小伙伴是不是有點蒙?
其實這倒無所謂,這段話看的蒙絲毫不影響你進(jìn)行JVM優(yōu)化的
你只要記住一個結(jié)論:如果你在代碼里大量用了類似上面的反射的東西,那么JVM就是會動態(tài)的去生成一些類放入Metaspace區(qū)域里的。
所以上面看到的那些奇怪的類,就是由于不停的執(zhí)行反射的代碼才生成的,如下圖所示。
8、JVM創(chuàng)建的奇怪類有什么玄機?
那么接下來我們就很奇怪一件事情,就是JVM為什么要不停的創(chuàng)建那些奇怪的類然后放入Metaspace中去?
其實這就要從一個點入手來分析一下了,因為上面說的那種JVM自己創(chuàng)建的奇怪的類,他們的Class對象都是SoftReference,也就是軟引用的。
大家可千萬別說連類的Class是什么都沒聽說過?簡單來說,每個類其實本身自己也是一個對象,就是一個Class對象,一個Class對象就代表了一個類。同時這個Class對象代表的類,可以派生出來很多實例對象。
舉例來說,Class Student,這就是一個類,他本身是由一個Class類型的對象表示的。
但是如果你走一個Student student = new Student(),這就是實例化了這個Student類的一個對象,這是一個Student類型的實例對象。
所以我們這里所說的Class對象,就是JVM在發(fā)射過程中動態(tài)生成的類的Class對象,他們都是SoftReference軟引用的。
所謂的軟引用,最早我們再一篇文章里說過,正常情況下不會回收,但是如果內(nèi)存比較緊張的時候就會回收這些對象。
那么SoftReference對象到底在GC的時候要不要回收是通過什么公式來判斷的呢?
是如下的一個公式:
clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB。
這個公式的意思就是說,“clock - timestamp”代表了一個軟引用對象他有多久沒被訪問過了,freespace代表JVM中的空閑內(nèi)存空間,SoftRefLRUPolicyMSPerMB代表每一MB空閑內(nèi)存空間可以允許SoftReference對象存活多久。
舉個例子,假如說現(xiàn)在JVM創(chuàng)建了一大堆的奇怪的類出來,這些類本身的Class對象都是被SoftReference軟引用的。
然后現(xiàn)在JVM里的空間內(nèi)存空間有3000MB,SoftRefLRUPolicyMSPerMB的默認(rèn)值是1000毫秒,那么就意味著,此時那些奇怪的SoftReference軟引用的Class對象,可以存活3000 * 1000 = 3000秒,就是50分鐘左右。
當(dāng)然上面都是舉例而已,大家都知道,一般來說發(fā)生GC時,其實JVM內(nèi)部或多或少總有一些空間內(nèi)存的,所以基本上如果不是快要發(fā)生OOM內(nèi)存溢出了,一般軟引用也不會被回收。
所以大家就知道了,按理說JVM應(yīng)該會隨著反射代碼的執(zhí)行,動態(tài)的創(chuàng)建一些奇怪的類,他們的Class對象都是軟引用的,正常情況下不會被回收,但是也不應(yīng)該快速增長才對。
9、為什么JVM創(chuàng)建的奇怪的類會不停的變多?
那么究竟為什么JVM創(chuàng)建的那些奇怪的類會不停的變多呢?
原因很簡單,因為文章開頭那個新手工程師不知道從哪里扒出來了SoftRefLRUPolicyMSPerMB這個JVM啟動參數(shù),他直接把這個參數(shù)設(shè)置為0了。
他想的是,一旦這個參數(shù)設(shè)置為0,任何軟引用對象就可以盡快釋放掉,不用留存,盡量給內(nèi)存釋放空間出來,這樣不就可以提高內(nèi)存利用效率了么?
真是想的很傻很天真。
實際上一旦這個參數(shù)設(shè)置為0之后,直接導(dǎo)致clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB這個公式的右半邊是0,就導(dǎo)致所有的軟引用對象,比如JVM生成的那些奇怪的Class對象,剛創(chuàng)建出來就可能被一次Young GC給帶著立馬回收掉一些。
如下圖所示。
比如JVM好不容易給你弄出來100個奇怪的類,結(jié)果因為你瞎設(shè)置軟引用的參數(shù),導(dǎo)致突然一次GC就給你回收掉幾十個類
接著JVM在反射代碼執(zhí)行的過程中,就會繼續(xù)創(chuàng)建這種奇怪的類,在JVM的機制之下,會導(dǎo)致這種奇怪類越來越多。
也許下一次gc又會回收掉一些奇怪的類,但是馬上JVM還會繼續(xù)生成這種類,最終就會導(dǎo)致Metaspace區(qū)域被放滿了,一旦Metaspace區(qū)域被占滿了,就會觸發(fā)Full GC,然后回收掉很多類,接著再次重復(fù)上述循環(huán),如下圖所示。
其實很多人會有一個疑問,到底為什么軟引用的類因為錯誤的參數(shù)設(shè)置被快速回收之后,就會導(dǎo)致JVM不停創(chuàng)建更多的新的類呢?
其實大家不用去扣這里的細(xì)節(jié),這里有大量的底層JDK源碼的實現(xiàn),異常復(fù)雜,要真的說清楚,得好幾篇文章才能講清楚JDK底層源碼的這些細(xì)節(jié)。
大家只要記住這個結(jié)論,明白這個道理就好。
10、如何解決這個問題?
雖然底層JDK的一些實現(xiàn)細(xì)節(jié)我們沒分析,但是大致梳理出來了一個思路,大家也很清楚問題所在和原因了
解決方案很簡單。在有大量反射代碼的場景下,大家只要把
- -XX:SoftRefLRUPolicyMSPerMB=0
這個參數(shù)設(shè)置大一些即可,千萬別讓一些新手同學(xué)設(shè)置為0,可以設(shè)置個1000,2000,3000,或者5000毫秒,都可以。
提高這個數(shù)值,就是讓反射過程中JVM自動創(chuàng)建的軟引用的一些類的Class對象不要被隨便回收,當(dāng)時我們優(yōu)化這個參數(shù)之后,就可以看到系統(tǒng)穩(wěn)定運行了。
基本上Metaspace區(qū)域的內(nèi)存占用是穩(wěn)定的,不會來回大幅度波動了。
當(dāng)前題目:記一次隱藏很深的JVM線上慘案的分析、排查、解決
分享網(wǎng)址:http://www.5511xx.com/article/coicpec.html


咨詢
建站咨詢
