? ? ? ?某天下午正在噼里啪啦的寫代碼時继谚,釘釘群瘋狂的發(fā)FullGC告警豪诲,登陸相關(guān)機(jī)器榆芦,jps -lv | grep 找到PID后,執(zhí)行 jstat -gccause pid 2000 pid顯示如下:
CG的原因是per space已滿墙懂,再執(zhí)行jmap -heap pid,輸出如下:
很明顯,Per space已經(jīng)用完了扮念,當(dāng)前使用的jdk版本為:Java(TM) SE Runtime Environment (build 1.6.0_29-b11)JVM參數(shù)如下:
永久代最大內(nèi)存為96M损搬,這個區(qū)域存儲了class字節(jié)碼以及一些常量信息,要溢出除非是以下幾種情況:
1柜与、該區(qū)域設(shè)置過小巧勤,根本無法裝載應(yīng)用的所需的所有class
2、應(yīng)用里大量動態(tài)生成class,如頻繁編譯jsp或大量使用動態(tài)代理生成許多proxy
3弄匕、應(yīng)用自定義classloader颅悉,頻繁load class
4、應(yīng)用大量生成字符串迁匠,并調(diào)用string.intern()
出問題的服務(wù)一個class平均大小在10k左右剩瓶,96M可以加載9800多個class,JVM6在初始化是會加載2000左右類城丧,應(yīng)用本身只有99個class[linux遞歸統(tǒng)計文件數(shù):ls -lR | grep "^-" |wc -l]延曙,加上引用的類在4000個左右,再加上一些字符串常量亡哄,大約在60M左右枝缔,因此不可能是第一個原因,該服務(wù)是基于Spring MVC的純后臺服務(wù)蚊惯,只有一個jsp管理頁面愿卸,不管是Spring的AOP還是其他一些動態(tài)代理拐辽,都是在程序啟動就生成好了的,因此也基本可排除第二個原因擦酌,剩下3俱诸, 4,在應(yīng)用的代碼層面赊舶,沒有顯示調(diào)string.intern()睁搭,也沒有顯示自定義classloader,但無法排除引用的代碼里是否有笼平,jmap 打印的信息看园骆,per區(qū)的確是占用99%,究竟是什么數(shù)據(jù)消耗內(nèi)存呢寓调,jvm既然有命令可以看各個區(qū)的消耗锌唾,應(yīng)該有命令可以查看永久帶的信息,jmap --help夺英,果然找到一個參數(shù) :
-permstat to print permanent generation statistics
執(zhí)行該命令晌涕,首先打印的是字符串占用的空間,20658 intern Strings occupying 2174792bytes痛悯,接下輸出滿屏的groovy/lang/GroovyClassLoader,該GroovyClassLoader只有3個instance余黎,卻有4800多條紀(jì)錄,也就是說這些相同的classloader在不斷l(xiāng)oad class到虛擬機(jī)载萌,直到per區(qū)內(nèi)存消耗完惧财。直覺告訴我,肯定是某個代碼static引用了GroovyClassLoader扭仁,并不斷觸發(fā)該類load class垮衷,問題點(diǎn)已經(jīng)找到,為了不影響線上業(yè)務(wù)乖坠,先重啟服務(wù)搀突,接下來去代碼里搜關(guān)鍵字:groovy,沒有結(jié)果瓤帚,正準(zhǔn)備通過maven打印依賴關(guān)系【mvndependency:tree】查找線索時描姚,突然想到有個工具servlet里使用groovy動態(tài)執(zhí)行一些java代碼,groovy是將腳本動態(tài)編譯后load到虛擬機(jī)執(zhí)行的戈次,自然會產(chǎn)生許多class轩勘,問題應(yīng)該就是這里了,接下來我們梳理下整個事情來龍去脈:
1 代碼里創(chuàng)建了一個static GroovyShell 怯邪,GroovyShell持有GroovyClassLoader
2 每次傳一些java代碼過來绊寻,調(diào)用GroovyShell.eval(xxx)執(zhí)行
3 ?GroovyShell動態(tài)編譯腳本,再調(diào)用GroovyClassLoader load到虛擬機(jī)執(zhí)行。
4 當(dāng)腳本執(zhí)行完澄步,此前動態(tài)生成class就沒有什么用了冰蘑,但字節(jié)碼還駐留在per區(qū),并且不會被卸載村缸,隨著eval調(diào)用次數(shù)增多祠肥, Per區(qū)內(nèi)存就一點(diǎn)點(diǎn)的被消耗完。
為什么這些無用的字節(jié)碼沒被JVM回收呢梯皿,這得從class卸載機(jī)制說起仇箱,JVM的Per區(qū)沒有單獨(dú)的Garbage collector,這個區(qū)域只是在老年代FullGC時順帶回收的东羹,一個class只有在滿足以下條件時剂桥,才能被JVM 卸載
1. 該類所有的實(shí)例已經(jīng)被回收
2. 該類的ClassLoder已經(jīng)被回收
3. 該類對應(yīng)的Java.lang.Class對象沒有任何對方被引用
我們的代碼中,因?yàn)镚roovyClassLoader被GroovyShell引用属提,而GroovyShell被應(yīng)用代碼static引用权逗,整個應(yīng)用運(yùn)行期間,該引用鏈一直都在冤议,無法滿足條件2斟薇,所以即使我們腳本已經(jīng)執(zhí)行完,但動態(tài)生成的字節(jié)碼還會一直駐留JVM直到內(nèi)存溢出求类。因?yàn)閖ava并沒有提供卸載class的接口奔垦,所以我們只能想辦法滿足上述3個條件,讓JVM在必要時卸載class尸疆,解決思路就是要打破上述引用鏈,方案如下:
1 將GroovyShell由static改為局部變量
2 將GroovyShell放到WeakReference里惶岭,既能避免重復(fù)創(chuàng)建寿弱,又支持JVM卸載class
由于問題代碼只是一個運(yùn)維型工具類,時間上不是很敏感按灶,直接采用第一種方案症革。在本地測試,以下代碼執(zhí)行循環(huán)到6000多次時Per區(qū)內(nèi)存溢出鸯旁,用Jconsole觀察噪矛,加載的類直線上升,JVM加:XX:+TraceClassLoading ?-XX:+TraceClassUnloading铺罢,控制臺只打印load lcass艇挨,不見unload ?class日志,信息如下:
而改成局部變量后韭赘,控制臺出現(xiàn)unload class日志缩滨,程序一直運(yùn)行直到完成,再無per區(qū)溢出。
許多運(yùn)行在JVM上的腳本語言(如Groovy)為我們帶來很多便利脉漏,如在不重啟服務(wù)的情況下苞冯,修改服務(wù)的運(yùn)行數(shù)據(jù),清空緩存等侧巨,但若使用不當(dāng)舅锄,則會帶來致命打擊,所以使用時請小心謹(jǐn)慎司忱。在java項目中巧娱,不管是從工程的可維護(hù)性還是運(yùn)行性能來講,都不建議大量使用腳本烘贴。
Note:Java8去掉Per gen禁添,改為metaspace,并且把stirng常量也挪到heap里桨踪,metaspace采用直接內(nèi)存老翘,會自動調(diào)節(jié)大小,該區(qū)域大小只受限于物理內(nèi)存锻离,因此不會再有PerGen溢出問題铺峭,
擴(kuò)展閱讀:jmap命令詳解? ?JVM老年代??jstat命令??GC cause參數(shù)解析