又抓了一個致使頻繁GC的鬼--數組動態擴容

概述

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

Demo

測試代碼以下:
image.png
正如我上面註釋裏寫的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就是新生代裏的對象spa

那何解

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

一塊兒來學習吧對象

PerfMa KO 系列課之 JVM 參數【Memory篇】blog

實戰:OOM 後我如何分析解決的內存

相關文章
相關標籤/搜索