深入JVM彻底剖析前面ygc越来越慢的case
阿里JVM團(tuán)隊(duì)的同學(xué)幫助從JVM層面繼續(xù)深入的剖析了下前面那個(gè)ygc越來越慢的case,分析文章相當(dāng)?shù)馁?#xff0c;思路清晰,工具熟練,JVM代碼熟練,請(qǐng)看這位同學(xué)(阿里JVM團(tuán)隊(duì):寒泉子)寫的文章,我轉(zhuǎn)載到這。
?
Demo分析
雖然這個(gè)demo代碼邏輯很簡(jiǎn)單,但是其實(shí)這是一個(gè)特殊的demo,并不簡(jiǎn)單,如果我們將XStream對(duì)象換成Object對(duì)象,會(huì)發(fā)現(xiàn)不存在這個(gè)問題,既然如此那有必要進(jìn)去看看這個(gè)XStream的構(gòu)造函數(shù)(請(qǐng)大家直接翻XStream的代碼,這里就不貼了)。
這個(gè)構(gòu)造函數(shù)還是很復(fù)雜的,里面會(huì)創(chuàng)建很多的對(duì)象,上面還有一些方法實(shí)現(xiàn)我就不貼了,總之都是在不斷構(gòu)建各種大大小小的對(duì)象,一個(gè)XStream對(duì)象構(gòu)建出來的時(shí)候大概好像有12M的樣子。
那到底是哪些對(duì)象會(huì)導(dǎo)致ygc不斷增長(zhǎng)呢,于是可能想到逐步替換上面這些邏輯,比如將最后一個(gè)構(gòu)造函數(shù)里的那些邏輯都禁掉,然后我們?cè)倥軠y(cè)試看看還會(huì)不會(huì)讓ygc不斷惡化,最終我們會(huì)發(fā)現(xiàn),如果我們直接使用如下構(gòu)造函數(shù)構(gòu)造對(duì)象時(shí),如果傳入的classloader是AppClassLoader,則會(huì)發(fā)現(xiàn)這個(gè)問題不再出現(xiàn)了,代碼如下:
public static void main(String[] args) throws Exception {int i=0;while (true) {XStream xs = new XStream(null,null, new ClassLoaderReference(XStreamTest.class.getClassLoader()),null, new DefaultConverterLookup());xs.toString();xs=null;} }是不是覺得很神奇,由此可見,這個(gè)classloader至關(guān)重要。
?
不得不說的類加載器
這里著重要說的兩個(gè)概念是初始類加載器和定義類加載器。舉個(gè)栗子說吧,AClassLoader->BClassLoader->CClassLoader,表示AClassLoader在加載類的時(shí)候會(huì)委托BClassLoader類加載器來加載,BClassLoader加載類的時(shí)候會(huì)委托CClassLoader來加載,假如我們使用AClassLoader來加載X這個(gè)類,而X這個(gè)類最終是被CClassLoader來加載的,那么我們稱CClassLoader為X類的定義類加載器,而AClassLoader和BClassLoader分別為X類的初始類加載器,JVM在加載某個(gè)類的時(shí)候?qū)@三種類加載器都會(huì)記錄,記錄的數(shù)據(jù)結(jié)構(gòu)是一個(gè)叫做SystemDictionary的hashtable,其key是根據(jù)ClassLoader對(duì)象和類名算出來的hash值,而value是真正的由定義類加載器加載的Klass對(duì)象,因?yàn)槌跏碱惣虞d器和定義類加載器是不同的classloader,因此算出來的hash值也是不同的,因此在SystemDictionary里會(huì)有多項(xiàng)key值的value都是指向同一個(gè)Klass對(duì)象。
那么JVM為什么要分這兩種類加載器呢,其實(shí)主要是為了快速找到已經(jīng)加載的類,比如我們已經(jīng)通過AClassLoader來觸發(fā)了對(duì)X類的加載,當(dāng)我們?cè)俅问褂肁ClassLoader這個(gè)類加載器來加載X這個(gè)類的時(shí)候就不需要再委托給BClassLoader去找了,因?yàn)榧虞d過的類在JVM里有這個(gè)類加載器的直接加載的記錄,只需要直接返回對(duì)應(yīng)的Klass對(duì)象即可。
?
Demo中的類加載器是否會(huì)加載類
我們的demo里發(fā)現(xiàn)構(gòu)建了一個(gè)CompositeClassLoader的類加載器,那到底有沒有用這個(gè)類加載器加載類呢,我們可以設(shè)置一個(gè)斷點(diǎn)在CompositeClassLoader的loadClass方法上,可以看到會(huì)進(jìn)入斷點(diǎn)。
可見確實(shí)有類加載的動(dòng)作,根據(jù)類加載委托機(jī)制,在這個(gè)Demo中我們能肯定類是交給AppClassLoader來加載的,這樣一來CompositeClassLoader就變成了初始類加載器,而AppClassLoader會(huì)是定義類加載器,都會(huì)在SystemDictionary里存在,因此當(dāng)我們不斷new XStream的時(shí)候會(huì)不斷new CompositeClassLoader對(duì)象,加載類的時(shí)候會(huì)不斷往SystemDictionary里插入記錄,從而使SystemDictionary越來越膨脹,那自然而然會(huì)想到如果GC過程不斷去掃描這個(gè)SystemDictionary的話,那隨著SystemDictionary不斷膨脹,那么GC的效率也就越低,抱著驗(yàn)證下猜想的方式我們可以使用perf工具來看看,如果發(fā)現(xiàn)cpu占比排前的函數(shù)都是操作SystemDictionary的,那就基本驗(yàn)證了我們的想法,下面是perf工具的截圖,基本證實(shí)了這一點(diǎn)。
?
SystemDictionary為什么會(huì)影響GC過程
想象一下這么個(gè)情況,我們加載了一個(gè)類,然后構(gòu)建了一個(gè)對(duì)象(這個(gè)對(duì)象在eden里構(gòu)建)當(dāng)一個(gè)屬性設(shè)置到這個(gè)類里,如果gc發(fā)生的時(shí)候,這個(gè)對(duì)象是不是要被找出來標(biāo)活才行,那么自然而然我們加載的類肯定是我們一項(xiàng)重要的gc root,這樣SystemDictionary就成為了gc過程中的被掃描對(duì)象了,事實(shí)也是如此,可以看vm的具體代碼(代碼在:SharedHeap::process_strong_roots,感興趣的同學(xué)可以直接翻這部分代碼)。
看上面的SH_PS_SystemDictionary_oops_do task就知道了,這個(gè)就是對(duì)SystemDictionary進(jìn)行掃描。
但是這里要說的是雖然有對(duì)SystemDictionary進(jìn)行掃描,但是ygc的過程并不會(huì)對(duì)SystemDictionary進(jìn)行處理,如果要對(duì)它進(jìn)行處理需要開啟類卸載的vm參數(shù),CMS算法下,CMS GC和Full GC在開啟CMSClassUnloadingEnabled的情況下是可能對(duì)類做卸載動(dòng)作的,此時(shí)會(huì)對(duì)SystemDictionary進(jìn)行清理,所以當(dāng)我們?cè)谂苌厦鎑emo的時(shí)候,通過jmap -dump:live,format=b,file=heap.bin <pid>命令執(zhí)行完之后,ygc的時(shí)間瞬間降下來了,不過又會(huì)慢慢回去,這是因?yàn)閖map的這個(gè)命令會(huì)做一次gc,這個(gè)gc過程會(huì)對(duì)SystemDictionary進(jìn)行清理。
?
修改VM代碼驗(yàn)證
很遺憾hotspot目前沒有對(duì)ygc的每個(gè)task做一個(gè)時(shí)間的統(tǒng)計(jì),因此無法直接知道是不是SH_PS_SystemDictionary_oops_do這個(gè)task導(dǎo)致了ygc的時(shí)間變長(zhǎng),為了證明這個(gè)結(jié)論,我特地修改了一下代碼,在上面的代碼上加了一行:
GCTraceTime t("SystemDictionary_OOPS_DO",PrintGCDetails,true,NULL);
然后重新編譯,跑我們的demo,測(cè)試結(jié)果如下:
2016-03-14T23:57:24.293+0800: [GC2016-03-14T23:57:24.294+0800: [ParNew2016-03-14T23:57:24.296+0800: [SystemDictionary_OOPS_DO, 0.0578430 secs] : 81920K->3184K(92160K), 0.0889740 secs] 81920K->3184K(514048K), 0.0900970 secs] [Times: user=0.27 sys=0.00, real=0.09 secs] 2016-03-14T23:57:28.467+0800: [GC2016-03-14T23:57:28.468+0800: [ParNew2016-03-14T23:57:28.468+0800: [SystemDictionary_OOPS_DO, 0.0779210 secs] : 85104K->5175K(92160K), 0.1071520 secs] 85104K->5175K(514048K), 0.1080490 secs] [Times: user=0.65 sys=0.00, real=0.11 secs] 2016-03-14T23:57:32.984+0800: [GC2016-03-14T23:57:32.984+0800: [ParNew2016-03-14T23:57:32.984+0800: [SystemDictionary_OOPS_DO, 0.1075680 secs] : 87095K->8188K(92160K), 0.1434270 secs] 87095K->8188K(514048K), 0.1439870 secs] [Times: user=0.90 sys=0.01, real=0.14 secs] 2016-03-14T23:57:37.900+0800: [GC2016-03-14T23:57:37.900+0800: [ParNew2016-03-14T23:57:37.901+0800: [SystemDictionary_OOPS_DO, 0.1745390 secs] : 90108K->7093K(92160K), 0.2876260 secs] 90108K->9992K(514048K), 0.2884150 secs] [Times: user=1.44 sys=0.02, real=0.29 secs]我們會(huì)發(fā)現(xiàn)YGC的時(shí)間變長(zhǎng)的時(shí)候,SystemDictionary_OOPS_DO的時(shí)間也會(huì)相應(yīng)變長(zhǎng)多少,因此驗(yàn)證了我們的說法。
有同學(xué)提到如果Demo代碼改成不new XStream,而是直接new CompositeClassLoader或CustomClassLoader則不會(huì)出問題,按照上面的分析也很容易解釋,就是因?yàn)槿绻苯觧ew CustomClassLoader的話,并沒觸發(fā)loadClass這動(dòng)作,而new XStream的話在構(gòu)造器里就在loadClass。
有同學(xué)提到在JDK 8中跑這個(gè)case不會(huì)出現(xiàn)問題,原因是:jdk8在對(duì)SystemDictionary掃描時(shí)做了優(yōu)化,增加了一層cache,大大減少了需要掃描的入口數(shù)。
?
相關(guān)文章:
一個(gè)jstack/jmap等不能用的case
上篇文章中ygc越來越慢的case的原因解讀
總結(jié)
以上是生活随笔為你收集整理的深入JVM彻底剖析前面ygc越来越慢的case的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 上篇文章中ygc越来越慢的case的原因
- 下一篇: 【随笔】JVM核心:JVM运行和类加载