上篇文章中ygc越来越慢的case的原因解读
上篇文章中附加了一個(gè)最近碰到的奇怪的case,有位同學(xué)看到這個(gè)后周末時(shí)間折騰了下,把原因給分析出來(lái)了,分析過(guò)程很贊(無(wú)論是思路,還是工具的使用),非常感謝這位同學(xué)(阿里的一位同事,花名叫彥貝),在征求他的同意后,我把他寫的整個(gè)問(wèn)題的分析文章轉(zhuǎn)載到這里。
文里圖比較多,手機(jī)上不方便看的話,建議大家電腦上直接訪問(wèn)http://hellojava.info看。
上分析工具VisualVM
在解決很多問(wèn)題的時(shí)候,工具起的作用往往是巨大的,很多時(shí)候通過(guò)工具分析,很快便能找到原因,但是這次并沒(méi)有,下圖是VisualVM觀察到Heap上的GC圖表。
從圖表中可以看出,Perm區(qū)空間基本水平,但是Old區(qū)空間成增長(zhǎng)態(tài)勢(shì)與YGCT時(shí)間增長(zhǎng)的倍率基本一致。熟悉YGC的朋友都知道YGC主要分為標(biāo)記和回收兩個(gè)階段,YGCT也是基于這2個(gè)階段統(tǒng)計(jì)的。由于每次回收的空間大小差不多,所以懷疑是標(biāo)記階段使用的時(shí)間比較長(zhǎng),下面回顧一下JVM的垃圾標(biāo)記方式。
?
JVM垃圾回收的標(biāo)記方法-枚舉根節(jié)點(diǎn)
在Java語(yǔ)言里面,可作為GC Roots的節(jié)點(diǎn)主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中。如果要使用可達(dá)性分析來(lái)判斷內(nèi)存是否可回收,那分析工作必須在一個(gè)能保障一致性的快照中進(jìn)行——這里“一致性”的意思是整個(gè)分析期間整個(gè)執(zhí)行系統(tǒng)看起來(lái)就像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上,不可以出現(xiàn)分析過(guò)程中,對(duì)象引用關(guān)系還在不斷變化的情況,這點(diǎn)不滿足的話分析結(jié)果準(zhǔn)確性就無(wú)法保證。這點(diǎn)也是導(dǎo)致GC進(jìn)行時(shí)必須“Stop The World”的其中一個(gè)重要原因,即使是號(hào)稱(幾乎)不會(huì)發(fā)生停頓的CMS收集器中,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。
由于目前的主流JVM使用的都是準(zhǔn)確式GC,所以當(dāng)執(zhí)行系統(tǒng)停頓下來(lái)之后,并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得到哪些地方存放著對(duì)象引用。在HotSpot的實(shí)現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這個(gè)目的,在類加載完成的時(shí)候,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來(lái),在JIT編譯過(guò)程中,也會(huì)在特定的位置記錄下棧里和寄存器里哪些位置是引用。這樣GC在掃描時(shí)就就可以直接得知這些信息了。
上面這段引用自《深入理解Java虛擬機(jī)》,用個(gè)圖簡(jiǎn)單表示一下,當(dāng)然圖也是源于書中:
基于對(duì)GC Roots的懷疑,猜測(cè)Old區(qū)中存在越來(lái)越多的gc root節(jié)點(diǎn),那什么樣的對(duì)象是root節(jié)點(diǎn)呢?不懂的我趕緊google了一下。
(不要看我紅色圈出來(lái)了,第一次看到這幾個(gè)嫌疑犯時(shí)我也拿不準(zhǔn)是哪個(gè))
為了進(jìn)一步驗(yàn)證是Old區(qū)的GC Roots造成YGCT 增加的,我們來(lái)做一次full gc,干掉Old區(qū)。代碼如下:
public class SlowYGC {public static void main(String[] args) throws Exception {int i= 0;while (true) {XStream xs = new XStream();xs.toString();xs = null;if(i++ % 10000 == 0){System.gc();}}} }可以看出Full GC后 YGCT銳減到初始狀態(tài)。那是Full GC到底回收了哪些對(duì)象?進(jìn)入到下一步,增加VM參數(shù)。
?
增加VM參數(shù)
為了看到Full GC前后對(duì)象的回收情況,我們?cè)黾酉旅?個(gè)VM參數(shù)
-XX:+PrintClassHistogramBeforeFullGC
-XX:+PrintClassHistogramAfterFullGC。
重新運(yùn)行上面的代碼,可以觀察到下面的日志:
上圖左邊是FGC前的對(duì)象情況,右邊是FGC后的情況,結(jié)合我之前給出的GC Root候選人中的Monitor鎖,相信你很快就找到20026個(gè)[Ljava.util.concurrent.ConcurrentHashMap$Segment對(duì)象幾乎被全部回收了,ConcurrentHashMap中正是通過(guò)Segment來(lái)實(shí)現(xiàn)分段鎖的。那什么邏輯會(huì)導(dǎo)致出現(xiàn)大量的Segment鎖對(duì)象。繼續(xù)看Full GC日志com.thoughtworks.xstream.core.util.CompositeClassLoader這個(gè)對(duì)象也差不多在FGC的時(shí)候全軍覆沒(méi),所以懷疑是ClassLoader引起的鎖競(jìng)爭(zhēng),下面在ClassLoader的源碼中繼續(xù)查找。
?
ClassLoader中的ConcurrentHashMap
?
這里有個(gè)拿鎖的動(dòng)作,跟進(jìn)去看看唄。
?
為了驗(yàn)證走到這塊邏輯下了一個(gè)斷點(diǎn)。剩下的就是putIfAbsent方法(這里就不詳細(xì)分析實(shí)現(xiàn)了)有興趣的同學(xué)可以看看源碼中CAS和tryLock的使用。
至此基本分析和定位出了YGCT原因,在去看看Xstream的構(gòu)造函數(shù)。
可以看出每次new XstreamI()的同時(shí)會(huì)new一個(gè)新的ClassLoader,基本上證明了上述懷疑和猜測(cè)。
推導(dǎo)一下按照上述分析,應(yīng)該是所有自定義的ClassLoader都會(huì)YGCT變長(zhǎng)的時(shí)間問(wèn)題,于是手寫一個(gè)ClassLoader驗(yàn)證一下。Java代碼如下:
public class TestClassLoader4YGCT {public static void main(String[] args) throws Exception{while(true){Object obj = newObject();obj.toString();obj = null;}}private static Object newObject() throws Exception{ClassLoader classLoader = new ClassLoader() {@Overridepublic Class<?> loadClass(String s) throws ClassNotFoundException {try{String fileName = s.substring(s.lastIndexOf(".") + 1)+ ".class";InputStream inputStream = getClass().getResourceAsStream(fileName);if(inputStream == null){return super.loadClass(s);}byte[] b = new byte[inputStream.available()];inputStream.read(b);return defineClass(s,b,0,b.length);}catch (Exception e){System.out.println(e);return null;}}};Class<?> obj = classLoader.loadClass("jvmstudy.classload.TestClassLoader4YGCT");return obj.newInstance();} }VM 參數(shù)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms512m -Xmx512m -Xmn100m -XX:+UseConcMarkSweepGC
?
GC日志
?
結(jié)論
當(dāng)大量new自定義的ClassLoader來(lái)加載時(shí),會(huì)產(chǎn)生大量ClassLoader對(duì)象以及parallelLockMap(ConcurrentHashMap)對(duì)象,導(dǎo)致產(chǎn)生大量的Segment分段鎖對(duì)象,大大增加了GC Roots的數(shù)量,導(dǎo)致YGC中的標(biāo)記時(shí)間變長(zhǎng)。如果能直接看到Y(jié)GCT的詳細(xì)使用情況,這個(gè)結(jié)論會(huì)顯得更加嚴(yán)謹(jǐn)。這只是我自己的一個(gè)推導(dǎo),并不一定是正確答案。
?
更多深入分析
深入JVM徹底剖析前面ygc越來(lái)越慢的case
?
總結(jié)
以上是生活随笔為你收集整理的上篇文章中ygc越来越慢的case的原因解读的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一个jstack/jmap等不能用的ca
- 下一篇: 深入JVM彻底剖析前面ygc越来越慢的c