又抓了一個導(dǎo)致頻繁GC的鬼--數(shù)組動態(tài)擴容
概述
本周有個同事過來咨詢一個比較詭異的gc問題,大概現(xiàn)象是,系統(tǒng)一直在做cms gc,但是老生代一直不降下去,但是執(zhí)行一次jmap -histo:live之后,也就是主動觸發(fā)一次full gc之后,通過jstat -gcutil來看老生代一下就降下去了,初看下理論上不太可能,因為full gc也會對old做回收,于是我要同事針對他們的場景寫了一個簡單的demo出來,然后果然還真能重現(xiàn),不過他的demo設(shè)置的Heap有32G,于是我通過慢慢調(diào)整,最終在很小的內(nèi)存下也能重現(xiàn)出來。
Demo
測試代碼如下:
正如我上面注釋里寫的JVM參數(shù),控制新生代200M,老生代300M,老生代使用率達到90%的時候觸發(fā)CMS GC,大家可以跑跑看,這種情況下會發(fā)現(xiàn)不斷做CMS GC,但是老生代就是不降下去,但是只要你主動觸發(fā)一次Full GC,老生代立馬就會回收。
當(dāng)allocateMemory方法執(zhí)行完之后,期待的結(jié)果是gc之后List及里面的byte數(shù)組都應(yīng)該被回收掉,可是事實并不是這樣的。
初步定位
這段代碼非常簡單,我翻來覆去地看著這段代碼,視圖想改變點什么,能讓問題出現(xiàn)峰回路轉(zhuǎn),我不斷地控制for循環(huán)的次數(shù)和每次分配的內(nèi)存大小,最終我將目標轉(zhuǎn)移到那個ArrayList上,List里有個數(shù)組,在add過程中如果發(fā)現(xiàn)數(shù)組不夠了,于是會進行擴容,那擴容就是創(chuàng)建新的數(shù)組,將老的對象放到新數(shù)組里,那我試想要是不做擴容會不會有問題?于是我開始調(diào)整ArrayList的初始化大小,當(dāng)我調(diào)到一定大小,保證在add過程中不會做擴容,問題真出現(xiàn)了反轉(zhuǎn),居然能正?;厥樟?,比如上面的demo,將數(shù)組長度設(shè)置為len,那結(jié)果就完全不一樣了,老生代很快就被回收了。
那目標能鎖定到數(shù)組擴容了
數(shù)組擴容
ArrayList里的數(shù)組擴容,使用的是System.arrayCopy調(diào)用,這是一個native方法,在java層面創(chuàng)建一個新的長度的數(shù)組,然后將老數(shù)組和新數(shù)組都傳進去,在native里將老數(shù)組里的元素指針拷貝到新數(shù)組里,其實做的是淺拷貝,反復(fù)看native這塊實現(xiàn),也基本解釋不通那個現(xiàn)象,一度懷疑我對GC的理解了,是不是有哪些細節(jié)沒有注意到。
經(jīng)過我內(nèi)存dump分析,發(fā)現(xiàn)上面Demo里的List對象確實被回收了,但是List里的數(shù)組沒有被回收,這個數(shù)組里的byte數(shù)組都沒有被回收。
原來是這個鬼
帶著百思不得其解的疑惑和我們組同事討論,看看還有沒有其他可能的沒考慮到疑惑點,開始也都覺得疑惑,后來傳勝突然想到會不會是存在跨代引用的問題,于是回過來仔細再想想每個步驟,好像還真有可能,因為傳給System.arrayCopy的新數(shù)組是在java層面構(gòu)建傳進來的,在新生代分配的可能性***,這樣再加上拷貝僅僅是淺拷貝,那么老生代里的byte數(shù)組因為存在新生代里新書組的引用,那僅僅做CMS GC就不可能回收這些老生代的對象了,因為CMS GC的一個gc root就是新生代里的對象。
那何解
至此終于抓出了那個鬼,于是想應(yīng)對策略,既然這樣,只要保證在cms gc回收old之前做一次ygc就能保證新生代里的那個新數(shù)組被回收而沒有指向老生代那些byte數(shù)組,那么這些數(shù)組就能正常被cms gc回收了,所以加上-XX:+CMSScavengeBeforeRemark即可解此問題。
【本文是51CTO專欄作者李嘉鵬的原創(chuàng)文章,轉(zhuǎn)載請通過微信公眾號(你假笨,id:lovestblog)聯(lián)系作者本人獲取授權(quán)】