問題產生
最近新上線的系統偶爾會報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中的類需要滿足什么條件才能夠被當成垃圾被卸載回收?
條件還是比較嚴苛的,需同時滿足如下三個條件的類才會被卸載:
-
該類所有的實例都已經被回收;
-
加載該類的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/
轉載自:石衫的架構筆記 微信公眾號