1.現(xiàn)象
為了降低gc時間涩澡,我們打算對一批服務安裝jdk11顽耳,使用zgc。在對zgc進行測試期間妙同,發(fā)現(xiàn)隨著程序的運行射富,gc時間越來越長。如下圖所示:
同時進程的gc次數(shù)并沒有發(fā)生太大變化粥帚,如下圖所示:
zgc的 gc.time 只和 GC Roots 相關胰耗,關于zgc可以參考:https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
既然gc次數(shù)沒有變多,所以應該是GC Roots 數(shù)量增長
2.原因分析
我們?nèi)ゲ榭唇鼛滋斓膅c日志芒涡,會發(fā)現(xiàn) Subphase: Pause Roots ClassLoaderDataGraph? 這個階段的耗時會不斷增加
這時候開始懷疑GC Roots有問題
此時查看metaspace元空間監(jiān)控柴灯,發(fā)現(xiàn)metaspace不斷增長,metaspace里的對象是被認為GC Roots费尽。所以因為metaspace不斷增長赠群,導致GC Roots越來越多,最終導致gc時間越來越長
為什么metasapce空間不斷增長呢旱幼?
metaspace空間主要保存對象類信息乎串,我們查看關于class相關監(jiān)控信息
可以看到我們程序不斷l(xiāng)oad class,但是沒有unload class,所以導致meta內(nèi)存一直增長
原因大概也就清楚了叹誉,但是此時產(chǎn)生疑問鸯两,為什么程序原先使用jdk8 cms gc算法,沒有這個問題呢长豁。
我們查看一下之前使用cms算法相關監(jiān)控
可以看到這臺 cms 機器钧唐,也不斷l(xiāng)oad class,但是他也會 unload class匠襟,所以metaspace一直維持平穩(wěn)
因為我們在使用cms時钝侠,設置了一個jvm參數(shù):CMSClassUnloadingEnabled 。該參數(shù)表示在進行cms酸舍,進行class卸載
3. zgc
為什么zgc不會收 meta:使用的jdk11不支持回收帅韧,jdk12才支持
在JDK11,zgc垃圾回收器目前還只是實驗性的功能啃勉,只支持Linux/x64平臺忽舟。后續(xù)優(yōu)化接改進,短時間內(nèi)無法更新到JDK11中淮阐,所以可能會遇到一些不穩(wěn)定因素叮阅。例如: 1. JDK12支持并發(fā)類卸載功能。2. JDK13將可回收內(nèi)存從4TB支持到16TB泣特。3. JDK14提升穩(wěn)定性的同時浩姥,提高性能。4. JDK15從實驗特性轉(zhuǎn)變?yōu)榭缮a(chǎn)特性 状您。所以對于一些大量使用反射勒叠、動態(tài)代理、CGLIB和Javasist等頻繁自定義類加載器的場景中膏孟,ZGC難以處理Metaspace的巨大內(nèi)存壓力缴饭。
4.為什么metaspace一直升高
我們對運行的進程進行class統(tǒng)計
jcmd {pid} GC.class_stats
發(fā)現(xiàn)存在大量的jdk.internal.reflect.GeneratedConstructorAccessor class類
我們對上述的內(nèi)容進行前綴統(tǒng)計,命令如下:
?jcmd {pid} GC.class_stats |awk '{print$13}'|sed? 's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1
發(fā)現(xiàn) jdk.internal.reflect 包名下的類文件骆莹,占用了很大一部分颗搂,并且隨著程序運行,jdk.internal.reflect這個類文件越來越多幕垦。
這個就是我們在使用jdk反射時丢氢,會自動生成的類字節(jié)碼,被jvm加載進metaspace先改。隨著程序運行疚察,加載類字節(jié)碼越來越多,但是沒有釋放仇奶,導致meta越來越大貌嫡。
5.解決辦法
-Dsun.reflect.inflationThreshold 可以控制通過反射生成字節(jié)碼。(該值表示 反射調(diào)用多少次 才開始生成字節(jié)碼)
當把該參數(shù)這是int 最大值時,說明永不生成字節(jié)碼岛抄。
從stackoverflows摘抄一段話
When using Java reflection, the JVM has two methods of accessing the information on the class being reflected. It can use a JNI accessor, or a Java bytecode accessor. If it uses a Java bytecode accessor, then it needs to have its own Java class and classloader (sun/reflect/GeneratedMethodAccessor class and sun/reflect/DelegatingClassLoader). Theses classes and classloaders use native memory. The accessor bytecode can also get JIT compiled, which will increase the native memory use even more. If Java reflection is used frequently, this can add up to a significant amount of native memory use. The JVM will use the JNI accessor first, then after some number of accesses on the same class, will change to use the Java bytecode accessor. This is called inflation, when the JVM changes from the JNI accessor to the bytecode accessor. Fortunately, we can control this with a Java property. The sun.reflect.inflationThreshold property tells the JVM what number of times to use the JNI accessor. If it is set to 0, then the JNI accessors are always used. Since the bytecode accessors use more native memory than the JNI ones, if we are seeing a lot of Java reflection, we will want to use the JNI accessors. To do this, we just need to set the inflationThreshold property to zero.
If you are on a Oracle JVM then you would only need to set:? -Dsun.reflect.inflationThreshold=2147483647
If you are on IBM JVM, then you would need to set:? ?-Dsun.reflect.inflationThreshold=0
大概意思說:使用java反射别惦,內(nèi)部實現(xiàn)有2種方式,一種是jni夫椭,另一種是生成字節(jié)碼方式掸掸。生成字節(jié)碼方式會占用更多內(nèi)存,但是性能會好一點蹭秋。sun.reflect.inflationThreshold 代表扰付,反射執(zhí)行多少次后,開始使用字節(jié)碼方式仁讨,當我們把這個參數(shù)設置為int最大值羽莺,代表永不使用字節(jié)碼方式,也就沒有內(nèi)存問題了洞豁。
具體原理參考:
重新加上?-Dsun.reflect.inflationThreshold=2147483647 這個參數(shù)盐固,后來gc.time 次數(shù)就一直穩(wěn)定下來了,并且metaspace空間也不增長了