又抓到一個導致頻繁GC的鬼——數組動態擴容

概述

本週有個同事過來諮詢一個比較詭異的gc問題,大概現象是,系統一直在做cms gc,但是老生代一直不降下去,但是執行一次jmap -histo:live之後,也就是主動觸發一次full gc之後,通過jstat -gcutil來看老生代一下就降下去了,初看下理論上不太可能,因爲full gc也會對old做回收,於是我要同事針對他們的場景寫了一個簡單的demo出來,然後果然還真能重現,不過他的demo設置的Heap有32G,於是我通過慢慢調整,最終在很小的內存下也能重現出來

Demo

測試代碼如下:

又抓到一個導致頻繁GC的鬼——數組動態擴容

 

正如我上面註釋裏寫的JVM參數,控制新生代200M,老生代300M,老生代使用率達到90%的時候觸發CMS GC,大家可以跑跑看,這種情況下會發現不斷做CMS GC,但是老生代就是不降下去,但是隻要你主動觸發一次Full GC,老生代立馬就會回收。當allocateMemory方法執行完之後,期待的結果是gc之後List及裏面的byte數組都應該被回收掉,可是事實並不是這樣的

初步定位

這段代碼非常簡單,我翻來覆去地看着這段代碼,試圖想改變點什麼,能讓問題出現峯迴路轉,我不斷地控制for循環的次數和每次分配的內存大小,最終我將目標轉移到那個ArrayList上,List裏有個數組,在add過程中如果發現數組不夠了,於是會進行擴容,那擴容就是創建新的數組,將老的對象放到新數組裏,那我試想要是不做擴容會不會有問題?於是我開始調整ArrayList的初始化大小,當我調到一定大小,保證在add過程中不會做擴容,問題真出現了反轉,居然能正常回收了,比如上面的demo,將數組長度設置爲len,那結果就完全不一樣了,老生代很快就被回收了,那麼目標就能鎖定到數組擴容了

數組擴容

ArrayList裏的數組擴容,使用的是System.arrayCopy調用,這是一個native方法,在java層面創建一個新的長度的數組,然後將老數組和新數組都傳進去,在native裏將老數組裏的元素指針拷貝到新數組裏,其實做的是淺拷貝,反覆看native這塊實現,也基本解釋不通那個現象,一度懷疑我對GC的理解了,是不是有哪些細節沒有注意到。經過我內存dump分析,發現上面Demo裏的List對象確實被回收了,但是List裏的數組沒有被回收,這個數組裏的byte數組都沒有被回收

原來是這個鬼導致的

帶着百思不得其解的疑惑和我們組同事討論,看看還有沒有其他可能的沒考慮到疑惑點,開始也都覺得疑惑,後來同事突然想到會不會是存在跨代引用的問題,於是回過來仔細再想想每個步驟,好像還真有可能,因爲傳給System.arrayCopy的新數組是在java層面構建傳進來的,在新生代分配的可能性最大,這樣再加上拷貝僅僅是淺拷貝,那麼老生代裏的byte數組因爲存在新生代裏新數組的引用,那僅僅做CMS GC就不可能回收這些老生代的對象了,因爲CMS GC的一個gc root就是新生代裏的對象

何解

至此終於抓出了那個鬼,於是想應對策略,既然這樣,只要保證在cms gc回收old之前做一次ygc就能保證新生代裏的那個新數組被回收而沒有指向老生代那些byte數組,那麼這些數組就能正常被cms gc回收了,所以加上-XX:+CMSScavengeBeforeRemark即可解此問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章