一次由groovy引起的fullGC問題排查
一、問題背景
二、分析過程
- 2.1 參數(shù)配置
- 2.2 定位過程
- 2.3 JVM分析
- 2.4 問題分析
三、解決方案
一、問題背景
prometheus監(jiān)控報警生效后,某服務(wù)每天的上午 8-12 點(diǎn)間會有fullGC的報警;
排查并解決該問題;
二、分析過程
2.1 參數(shù)配置
JVM 參數(shù)配置如下:
新生代大?。?G;
新生代垃圾收集器:ParNewGC;
老年代大?。?G;
老年代垃圾收集器:ConcMarkSweepGC;
CMS觸發(fā)條件:老年代內(nèi)存占用達(dá)到80%及以上;
2.2 定位問題
1.由于報警的時間點(diǎn)都集中在上午的 8-12 點(diǎn)之間,懷疑是由于某個定時任務(wù)造成的;
2.定位具體的定時任務(wù),有兩個定時任務(wù)的時間設(shè)置基本滿足;
3.確定具體的任務(wù)
確認(rèn)的兩個思路:
1.通過日志確認(rèn)定時任務(wù)的執(zhí)行時長等;
2.將2個定時任務(wù)分別指定不同的機(jī)器執(zhí)行觀察;
排查任務(wù)執(zhí)行時間:
任務(wù)1 : 很快,幾乎不處理業(yè)務(wù)邏輯;
任務(wù)2: 執(zhí)行約35分鐘時間;
8:10分開始,8:45分結(jié)束;
基本確定為第二個定時任務(wù)導(dǎo)致FullGC;
2.3 JVM分析
2.3.1 單天監(jiān)控圖
內(nèi)存趨勢
GC趨勢
2.3.2 報警時間段監(jiān)控圖
內(nèi)存趨勢
GC趨勢
2.3.3 圖表分析
2.3.3.1 老年代變化
現(xiàn)象
1.任務(wù)執(zhí)行過程中:老年代有明顯增長,并且FullGC后并沒有特別明顯的下降,只有些許下降;
2.任務(wù)執(zhí)行結(jié)束后:下次任務(wù)開始執(zhí)行,進(jìn)行FullGC后,會降到跟其他機(jī)器一樣的水平,甚至內(nèi)存占用更低;
備注
新生代到老年代的幾種情況
1:大對象;
2:年齡足夠長,cms沒有設(shè)置,默認(rèn)是6,通過jinfo確認(rèn)也是6;
3:suvivor區(qū)不足以存放YGC后的存活對象,直接使用擔(dān)保策略晉升到老年代;
分析
任務(wù)執(zhí)行過程中,YGC平均1分鐘執(zhí)行5次,很多對象都會達(dá)到最大晉升年齡6,晉升到老年代;
并且由于任務(wù)沒有結(jié)束,對象還有引用,所以FullGC之后并沒有明顯下降;
上次任務(wù)結(jié)束后,老年代并沒有像suvivor區(qū)一樣有一段時間的低內(nèi)存占用,主要是直到下次任務(wù)開始后才會觸發(fā)新一次的FullGC,觸發(fā)后,老年代的對象由于任務(wù)結(jié)束后沒有引用了,所以會正?;厥?;
2.3.3.2 survivor區(qū)變化
suvivor區(qū)內(nèi)存總共100M,任務(wù)執(zhí)行過程中,平均占用 80M;高的時候會飆升到90以上,所以這個過程中YGC也變得很頻繁,平均1分鐘5次;
2.3.3.3 非堆內(nèi)存/方法區(qū)/compressed class cach變化
使用 jstat 分別統(tǒng)計了兩臺機(jī)器的gc統(tǒng)計,兩者最大的區(qū)別在于 執(zhí)行過定時任務(wù)的機(jī)器的MC(方法區(qū)大小) 以及 CCSC(壓縮類空間大小) 明顯比沒有執(zhí)行過定時任務(wù)的機(jī)器高很多;
任務(wù)執(zhí)行過程中方法區(qū)的內(nèi)存占用會跟老年代的曲線保持一致,這幾個區(qū)的回收也是靠老年代,這個通過grafana平臺的監(jiān)控圖也可以看出來;
2.3.3.4 dump文件分析
groovy相關(guān)的類占比57.57%;
2.4 參數(shù)配置
java 與 groovy 版本
代碼中使用到groovy的地方:同樣是這個定時任務(wù),下發(fā)任務(wù)時,表達(dá)式檢驗(yàn)是否滿足下發(fā)條件,表達(dá)式是用groovy進(jìn)行處理的;
基本上可以定位問題在groovy腳本的加載處,groovy不合理使用會導(dǎo)致,動態(tài)生成很多新類,使得metaspace的不斷被占用;
class 對象在 1.8 及以后存放在 metaspace 中,也就是堆外內(nèi)存。
groovy每執(zhí)行一次,會將傳入的文本動態(tài)加載成一個腳本類,入?yún)⑹俏谋緯r,生成的文件名中包含了一個自增的數(shù)值,也就是每執(zhí)行一次都會動態(tài)生成一個新類,1個用戶7個任務(wù)規(guī)則校驗(yàn) * 15962個用戶 = 111734個
GroovyShell 在內(nèi)部,它使用groovy.lang.GroovyClassLoader,這是在運(yùn)行時編譯和加載類的核心。
GroovyClassLoader 保留對其創(chuàng)建的所有類的引用,而 class 對象只有在被加載的 classloader 被回收的時候才會被回收,因此很容易造成內(nèi)存泄漏;
綜上分析,groovy 錯誤的使用方式導(dǎo)致 class 對象常駐堆外內(nèi)存且隨著調(diào)用頻率增長。
三、解決方案
1、每個腳本共用一個 GroovyShell 對象,不能使用 for 的方式,循環(huán)創(chuàng)建使用;
2、每次執(zhí)行完釋放對象 shell.getClassLoader().clearCache();