JVM源碼分析之自定義類加載器如何拉長YGC

概述

本文重點講述畢玄大師在其公衆號上發的一個GC問題一個jstack/jmap等不能用的case,對於畢大師那篇文章,題目上沒有提到GC的那個問題,不過進入到文章裏能夠看到,既然文章提到了jstack/jmap的問題,這裏也簡單回答下jstack/jmap沒法使用的問題,其實最多見的場景是使用jstack/jmap的用戶和目標進程不是同一個用戶,哪怕你執行jstack/jmap的動做是root用戶也無濟於事,不過畢大師這裏主要提到的是jmap -heap/histo這兩個參數帶來的問題,若是使用-heap/histo的參數,其實和你們使用-F參數是同樣的,底層都是經過serviceability agent來實現的,並非jvm attach的方式,經過sa連上去以後會掛起進程,在serviceability agent裏存在bug可能致使detach的動做不會被執行,從而會讓進程一直掛着,能夠經過top命令驗證進程是否處於T狀態,若是是說明進程被掛起了,若是進程被掛起了,能夠經過kill -CONT [pid]來恢復。算法

再回到那個GC的問題,用的參數以下:數據結構

image.png

demo程序以下:jvm

image.png

執行效果以下函數

image.png

發現gc的時間愈來愈長,可是gc觸發的時機以及回收的效果都差很少,那問題究竟在哪裏呢?工具

Demo分析

雖然這個demo代碼邏輯很簡單,可是其實這是一個特殊的demo,並不簡單,若是咱們將XStream對象換成Object對象,會發現不存在這個問題,既然如此那有必要進去看看這個XStream的構造函數:image.pngoop

image.png

這個構造函數仍是很複雜的,裏面會建立不少的對象,上面還有一些方法實現我就不貼了,總之都是在不斷構建各類大大小小的對象,一個XStream對象構建出來的時候大概好像有 12M 的樣子。測試

那究竟是哪些對象會致使 ygc 不斷增加呢,因而可能想到逐步替換上面這些邏輯,好比將最後一個構造函數裏的那些邏輯都禁掉,而後咱們再跑測試看看還會不會讓ygc不斷惡化,最終咱們會發現,若是咱們直接使用以下構造函數構造對象時,若是傳入的classloader是AppClassLoader,那會發現這個問題再也不出現了。3d

image.png

測試代碼以下:日誌

image.png

gc日誌以下:code

image.png

是否是以爲很神奇,因而可知,這個classloader相當重要。

不得不說的類加載器

這裏着重要說的兩個概念是初始類加載器定義類加載器。舉個栗子說吧,AClassLoader->BClassLoader->CClassLoader,表示AClassLoader在加載類的時候會委託BClassLoader類加載器來加載,BClassLoader加載類的時候會委託CClassLoader來加載,假如咱們使用AClassLoader來加載X這個類,而X這個類最終是被CClassLoader來加載的,那麼咱們稱CClassLoader爲X類的定義類加載器,而AClassLoader和BClassLoader分別爲X類的初始類加載器,JVM在加載某個類的時候對這三種類加載器都會記錄,記錄的數據結構是一個叫作SystemDictionary的hashtable,其key是根據ClassLoader對象和類名算出來的hash值,而value是真正的由定義類加載器加載的Klass對象,由於初始類加載器和定義類加載器是不一樣的classloader,所以算出來的hash值也是不一樣的,所以在SystemDictionary裏會有多項值的value都是指向同一個Klass對象。

那麼JVM爲何要分這兩種類加載器呢,其實主要是爲了快速找到已經加載的類,好比咱們已經經過AClassLoader來觸發了對X類的加載,當咱們再次使用AClassLoader這個類加載器來加載X這個類的時候就不須要再委託給BClassLoader去找了,由於加載過的類在JVM裏有這個類加載器的直接加載的記錄,只須要直接返回對應的Klass對象便可。

Demo中的類加載器是否會加載類

咱們的demo裏發現構建了一個CompositeClassLoader的類加載器,那到底有沒有用這個類加載器加載類呢,咱們能夠設置一個斷點在CompositeClassLoader的loadClass方法上,因而看到下面的堆棧:

image.png

可見確實有類加載的動做,根據類加載委託機制,在這個demo中咱們能確定類是交給AppClassLoader來加載的,這樣一來CompositeClassLoader就變成了初始類加載器,而AppClassLoader會是定義類加載器,都會在SystemDictionary裏存在,所以當咱們不斷new XStream的時候會不斷new CompositeClassLoader對象,加載類的時候會不斷往SystemDictionary裏插入記錄,從而使SystemDictionary愈來愈膨脹,那天然而然會想到若是GC過程不斷去掃描這個SystemDictionary的話,那隨着SystemDictionary不斷膨脹,那麼GC的效率也就越低,抱着驗證下猜測的方式咱們可使用perf工具來看看,若是發現cpu佔比排前的函數若是都是操做SystemDictionary的,那就基本驗證了咱們的說法,下面是perf工具的截圖,基本證明了這一點。

image.png

SystemDictionary爲何會影響GC過程

想象一下這麼個狀況,咱們加載了一個類,而後構建了一個對象(這個對象在eden裏構建)當一個屬性設置到這個類裏,若是gc發生的時候,這個對象是否是要被找出來標活才行,那麼天然而然咱們加載的類確定是咱們一項重要的gc root,這樣SystemDictionary就成爲了gc過程當中的被掃描對象了,事實也是如此,能夠看vm的具體代碼:

image.png

image.png

看上面的SH_PS_SystemDictionary_oops_do task就知道了,這個就是對SystemDictionary進行掃描。

可是這裏要說的是雖然有對SystemDictionary進行掃描,可是ygc的過程並不會對SystemDictionary進行處理,若是要對它進行處理須要開啓類卸載的vm參數,CMS算法下,CMS GC和Full GC在開啓CMSClassUnloadingEnabled的狀況下是可能對類作卸載動做的,此時會對SystemDictionary進行清理,因此當咱們在跑上面demo的時候,經過jmap-dump:live,format=b,file=heap.bin 命令執行完以後,ygc的時間瞬間降下來了,不過又會慢慢回去,這是由於jmap的這個命令會作一次gc,這個gc過程會對SystemDictionary進行清理。

修改VM代碼驗證

很遺憾hotspot目前沒有對ygc的每一個task作一個時間的統計,所以沒法直接知道是否是SHPSSystemDictionaryoopsdo這個task致使了ygc的時間變長,爲了證實這個結論,我特意修改了一下代碼,在上面的代碼上加了一行:

image.png

而後從新編譯,跑咱們的demo,測試結果以下:

image.png

咱們會發現YGC的時間變長的時候,SystemDictionaryOOPSDO的時間也會相應變長多少,所以驗證了咱們的說法。

推薦閱讀:
聽說99.99%的人都會答錯的類加載的問題
消失的死鎖

相關文章
相關標籤/搜索