問題產生java
最近新上線的系統偶爾會報FullGC時間過長(>1s)的告警,查看GC日誌,以下圖所示:bash
看到GC日誌,我第一時間關注到的不是GC耗時,而是GC觸發的緣由:Metadata GC Threshold數據結構
也就是 FullGC 觸發的緣由是由於Metaspace大小達到了GC閾值。在監控系統裏面看了一下Metaspace的大小變化趨勢,以下圖所示:架構
按照以往的經驗,Metaspace在系統穩定運行一段時間後佔用空間應該比較穩定纔對,可是從上圖來看,Metaspace顯然是呈現大幅波動。爲何呢? 框架
相關知識
性能
咱們知道Metaspace主要存儲類的元數據,好比咱們加載了一個類,那麼這個類的信息就會按照必定的數據結構存儲在Metaspace中。學習
Metaspace的大小和加載類的數目有很大關係,加載的類越多,Metaspace佔用內存也就越大。spa
Metaspace被分配於堆外空間,默認最大空間只受限於系統物理內存。跟它相關的比較重要的兩個JVM參數:日誌
-XX:MetaspaceSize -XX:MaxMetaspaceSize複製代碼
MaxMetaspaceSize,你們從名字也能猜到是指Metaspace最大值。code
而MetaspaceSize可能就比較容易讓人誤解爲是Metaspace的最小值,其實它是指Metaspace擴容時觸發FullGC的初始化閾值。
在GC後Metaspace會被動態調整:若是本次GC釋放了大量空間,那麼就適當下降該值,若是釋放的空間較小則適當提升該值,固然它的值不會大於MaxMetaspaceSize.
另一個相關知識是:Metaspace中的類須要知足什麼條件纔可以被當成垃圾被卸載回收?
條件仍是比較嚴苛的,需同時知足以下三個條件的類纔會被卸載:
該類全部的實例都已經被回收;
加載該類的ClassLoader已經被回收;
該類對應的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引用的對象是否被回收:
SoftReference對象實例多久未訪問,經過clock - timestamp得出對象大概有多久未訪問;
內存空閒空間的大小;
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/
我的公衆號:石杉的架構筆記(ID:shishan100)
歡迎長按下圖關注公衆號:石杉的架構筆記!
公衆號後臺回覆資料,獲取做者獨家祕製學習資料
石杉的架構筆記,BAT架構經驗傾囊相授