警惕大量類(lèi)加載器的創(chuàng)建導(dǎo)致詭異的Full GC
言歸正傳,今天有個(gè)同事找我,其實(shí)好像之前就找過(guò)我,一直因?yàn)樘Γ竺婢屯浰氖铝耍浇裉爝€沒(méi)查出原因就又找了過(guò)來(lái),現(xiàn)象是系統(tǒng)老是進(jìn)行Full GC,在啟動(dòng)沒(méi)過(guò)多久就會(huì)發(fā)生Full GC,這個(gè)現(xiàn)象相對(duì)比較少見(jiàn)的,于是找他要了GC日志,赫然看到如下日志:
這個(gè)很顯然就是達(dá)到了Metaspace的閾值觸發(fā)的Full GC了,但是看看Metaspace的size,使用了134M左右,于是我詢(xún)問(wèn)他MetaspaceSize和MaxMetaspaceSize分別設(shè)置了多少,告知我設(shè)置的是256M,那就有幾個(gè)比較奇怪的地方了:
- 為什么啟動(dòng)沒(méi)多久就因?yàn)镸etaspace觸發(fā)了Full GC
- 從使用率來(lái)看并沒(méi)有達(dá)到閾值
- 在Full GC之后立馬就能正常運(yùn)行一段時(shí)間,說(shuō)明Metaspace確實(shí)回收了
先說(shuō)個(gè)JVM的BUG
從上面的GC日志,我們看到了Full GC前后,Metaspace的使用變化是從137752K->71671K,其實(shí)你們?nèi)绻玫膐racle官方的JDK,看到的會(huì)是137752K->137752K,也就是并沒(méi)有發(fā)生變化,看起來(lái)好像Metaspace并沒(méi)有被回收,其實(shí)這是JVM的一個(gè)BUG,我們alijdk將這個(gè)問(wèn)題進(jìn)行了修復(fù),能看到前后是有變化的,所以如果大家在排查Metaspace的問(wèn)題時(shí)候,希望不要被這個(gè)信息騙到
再聊點(diǎn)GC日志
從JDK8開(kāi)始,任何GC,都會(huì)默認(rèn)打印GC Cause,所以你看到上面的Full GC是因?yàn)镸etadata GC Threshold觸發(fā)的,也就是Metaspace committed的內(nèi)存加上這次要分配的內(nèi)存達(dá)到了MetaspaceSize的閾值。如果是JDK7(之前版本不支持),那可以通過(guò)加JVM參數(shù)-XX:+PrintGCCause來(lái)打印原因,可以通過(guò)下面的圖片小程序鏈接點(diǎn)進(jìn)去看看這個(gè)參數(shù)的具體用法及含義:
再提一點(diǎn),Metaspce觸發(fā)的GC都是Full GC。
另外大家常看到的類(lèi)似下面的Allocation Failure的GC Cause,其實(shí)是正常的,因?yàn)榇蟛糠諫C,尤其是YGC,都是因?yàn)榉峙鋬?nèi)存失敗才觸發(fā)的,所以不要認(rèn)為看到Failure就覺(jué)得有問(wèn)題。
為何使用率這么低就觸發(fā)了Full GC
Metaspace觸發(fā)Full GC,是因?yàn)镸etaspace committed的內(nèi)存加上這次要分配的內(nèi)存之和超過(guò)了閾值才會(huì)觸發(fā),但是我們看使用了才134M,而閾值卻是256M,那可能懷疑下面兩種情況:
這次分配的內(nèi)存達(dá)到122M以上?
碎片化問(wèn)題?
對(duì)于***種情況,基本不太可能,因?yàn)橐粋€(gè)類(lèi)不可能要這么大內(nèi)存,所以暫時(shí)先排除這種可能。
對(duì)于第二種情況,有一個(gè)場(chǎng)景是能滿(mǎn)足的,類(lèi)加載器創(chuàng)建非常多,但是每個(gè)類(lèi)加載器加載的類(lèi)又特別少,同時(shí)Full GC之后又能很快被回收掉
為了驗(yàn)證第二種情況,我嘗試加兩個(gè)參數(shù)-XX:+HeapDumpBeforeFullGC和-XX:+HeapDumpAfterFullGC,在Full GC前后分別對(duì)內(nèi)存做一個(gè)dump,這兩個(gè)參數(shù),可以通過(guò)下面的圖片小程序鏈接看到具體的使用情況
從兩個(gè)dump的分析結(jié)果來(lái)看,查了下類(lèi)加載器的情況,果然在Full GC之前看到了31650個(gè)類(lèi)加載器,而Full GC之后,類(lèi)加載器個(gè)數(shù)變成了872個(gè),于是開(kāi)始找究竟是哪些類(lèi)加載器,最終發(fā)現(xiàn)某個(gè)特定類(lèi)型的類(lèi)加載器對(duì)象非常之多,咨詢(xún)了業(yè)務(wù)方確實(shí)存在這種情況,因?yàn)闆](méi)有做好緩存,所以導(dǎo)致了無(wú)止境創(chuàng)建
類(lèi)加載器過(guò)多為什么會(huì)導(dǎo)致Full GC
類(lèi)加載器創(chuàng)建過(guò)多,帶來(lái)的一個(gè)問(wèn)題是,在類(lèi)加載器***次加載類(lèi)的時(shí)候,會(huì)在Metaspace里會(huì)給它分配內(nèi)存塊,為了分配高效,每個(gè)類(lèi)加載器用來(lái)存放類(lèi)信息的內(nèi)存塊都是獨(dú)立的,所以哪怕你這個(gè)類(lèi)加載器只加載一個(gè)類(lèi),也會(huì)為之分配一塊空的內(nèi)存給這個(gè)類(lèi)加載器,其實(shí)是至少兩個(gè)內(nèi)存塊,于是你有可能會(huì)發(fā)現(xiàn)Metaspace的內(nèi)存使用率非常低,但是committed的內(nèi)存已經(jīng)達(dá)到了閾值,從而觸發(fā)了Full GC,如果這種只加載很少類(lèi)的類(lèi)加載器非常多,那造成的后果就是很多碎片化的內(nèi)存
JVMPocket介紹
JVMPocket是我最近搗鼓的一個(gè)微信小程序,大家可以通過(guò)搜索JVMPocket或者從我公眾號(hào)菜單里進(jìn)入,該小程序主要緣因JVM參數(shù)而誕生,有人問(wèn)我相關(guān)的問(wèn)題,告訴他們什么參數(shù)可以解決,但是苦于參數(shù)太長(zhǎng)而無(wú)法記住,特尷尬,有了JVMPocket之后,直接找到對(duì)應(yīng)的參數(shù)發(fā)個(gè)鏈接過(guò)去就可以看到對(duì)應(yīng)參數(shù)的具體含義,用法,默認(rèn)值以及大家的使用建議等,希望該小程序也能幫到大家,大家如果自己的JVM參數(shù)經(jīng)驗(yàn),都可以到對(duì)應(yīng)的參數(shù)下面留言讓更多人知道它背后的故事。
JVMPocket
JVM參數(shù)錦囊
【本文是51CTO專(zhuān)欄作者李嘉鵬的原創(chuàng)文章,轉(zhuǎn)載請(qǐng)通過(guò)微信公眾號(hào)(你假笨,id:lovestblog)聯(lián)系作者本人獲取授權(quán)】