一次 JVM FullGC 的排查過程及解決方案!---字節觀


 

問題產生

最近新上線的系統偶爾會報FullGC時間過長(>1s)的告警,查看GC日志,如下圖所示:

 

 

 

看到GC日志,我第一時間關注到的不是GC耗時,而是GC觸發的原因:Metadata GC Threshold

 

也就是 FullGC 觸發的原因是因為Metaspace大小達到了GC閾值。在監控系統里面看了一下Metaspace的大小變化趨勢,如下圖所示:

 

 

 

按照以往的經驗,Metaspace在系統穩定運行一段時間后占用空間應該比較穩定才對,但是從上圖來看,Metaspace顯然是呈現大幅波動。為什么呢? 

 

 

相關知識

我們知道Metaspace主要存儲類的元數據,比如我們加載了一個類,那么這個類的信息就會按照一定的數據結構存儲在Metaspace中。

 

Metaspace的大小和加載類的數目有很大關系,加載的類越多,Metaspace占用內存也就越大。

 

Metaspace被分配於堆外空間,默認最大空間只受限於系統物理內存。跟它相關的比較重要的兩個JVM參數:

 

-XX:MetaspaceSize -XX:MaxMetaspaceSize

 

MaxMetaspaceSize,大家從名字也能猜到是指Metaspace最大值。

 

而MetaspaceSize可能就比較容易讓人誤解為是Metaspace的最小值,其實它是指Metaspace擴容時觸發FullGC的初始化閾值。

 

在GC后Metaspace會被動態調整:如果本次GC釋放了大量空間,那么就適當降低該值,如果釋放的空間較小則適當提高該值,當然它的值不會大於MaxMetaspaceSize.

 

另外一個相關知識是:Metaspace中的類需要滿足什么條件才能夠被當成垃圾被卸載回收?

 

條件還是比較嚴苛的,需同時滿足如下三個條件的類才會被卸載:

 

  1. 該類所有的實例都已經被回收;

     

  2. 加載該類的ClassLoader已經被回收;

     

  3. 該類對應的java.lang.Class對象沒有任何地方被引用。

 

 

 

排查過程

我們可以回過頭再細看GC日志,可以看出Metaspace已使用內存在FullGC后明顯變小(372620K -> 158348K),說明Metaspace經過FGC后卸載了很多類。

 

從這點來看,我們有理由懷疑系統可能在頻繁地生成大量”一次性“的類,導致Metaspace所占用空間不斷增長,增長到GC閾值后觸發FGC。

 

 

 

那么這些被回收的類是什么呢?

 

為了弄清楚這點,我增加了如下兩個JVM啟動參數來觀察類的加載、卸載信息:

 

-XX:TraceClassLoading -XX:TraceClassUnloading

 

加了這兩個參數后,系統跑了一段時間,從Tomcat的catalina.out日志中發現大量如下的日志:

 

 

 

 

 

 

到此基本可以確定Metaspace增長的元凶是這些類,那么這些類

sun.reflect.GeneratedSerializationConstructorAccessorXXX

是干嘛的呢?又是從哪里引進來的呢?我也是一臉懵逼~~ 

 

根據類名Google了一把,找到了@寒泉子寫的《從一起GC血案談到反射原理》,這篇文章對這些類的來源解釋得很透徹。在這里我簡單總結如下:

 

Method method = XXX.class.getDeclaredMethod(xx,xx);method.invoke(target,params);

 

這些類的來源是反射,類似上面所示的反射代碼應該大家都寫過或者看過,我們常用的大多數框架比如Spring、Dubbo等都大量使用反射。

 

出於性能的考慮,JVM會在反射代碼執行一定次數后,通過動態生成一些類來將”反射調用”變為“非反射調用”,以達到性能更好。而這些動態生成的類的實例是通過軟引用SoftReference來引用的。

 

我們知道,一個對象只有軟引用SoftReference,如果內存空間不足,就會回收這些對象的內存;如果內存空間足夠,垃圾回收器不會回收它。只要垃圾回收器沒有回收它,該對象就可以被使用。

 

那么,究竟在什么時候會被回收呢?

 

SoftReference中有一個全局變量clock代表最后一次GC的時間點,有一個屬性timestamp,每次訪問SoftReference時,會將timestamp其設置為clock值。

 

當GC發生時,以下幾個因素影響SoftReference引用的對象是否被回收:

 

  1. SoftReference對象實例多久未訪問,通過clock - timestamp得出對象大概有多久未訪問;

     

  2. 內存空閑空間的大小;

     

  3. SoftRefLRUPolicyMSPerMB常量值;

 

是否保留SoftReference引用對象的判斷參考表達式,true為不回收,false為回收:

 

clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB

 

說明:

  • clock - timestamp:最后一次GC時間和SoftReference對象實例timestamp的屬性的差。就是這個SoftReference引用對象大概有多久未訪問過了

     

  • freespace:JVMHeap中空閑空間大小,單位為MB。

     

  • SoftRefLRUPolicyMSPerMB:每1M空閑空間可保持的SoftReference對象生存的時長(單位ms)。這個參數就是一個常量,默認值1000,可以通過參數:-XX:SoftRefLRUPolicyMSPerMB進行設置。

 

查看了一下我們系統的JVM參數配置,發現我們把SoftRefLRUPolicyMSPerMB設置為0了,這樣就導致軟引用對象很快就被回收了。進而導致需要頻繁重新生成這些動態類。

 

為了驗證這個猜測,我把SoftRefLRUPolicyMSPerMB改成了6000進行觀察,發現果然猜得沒錯。

 

系統啟動后不久Metaspace的使用空間基本保持不變了,運行幾天后也沒再出現因為Metaspace大小達到閾值而觸發FGC。至此問題解決。

 

 

References

 

假笨說-從一起GC血案談到反射原理: 

https://mp.weixin.qq.com/s/5H6UHcP6kvR2X5hTj_SBjA?

Java的強引用,軟引用,弱引用,虛引用及其使用場景: 

http://blogxin.cn/2017/09/16/java-reference/

轉載自:石衫的架構筆記 微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM