謹防JDK8重複類定義形成的內存泄漏

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網算法

概述

現在JDK8成了主流,你們都緊鑼密鼓地進行着升級,享受着JDK8帶來的各類便利,然而有時候升級並無那麼順利?好比說今天要說的這個問題。咱們都知道JDK8在內存模型上最大的改變是,放棄了Perm,迎來了Metaspace的時代。若是你對Metaspace還不熟,以前我寫過一篇介紹Metaspace的文章,你們有興趣的能夠看看我前面的那篇文章。緩存

咱們以前通常在系統的JVM參數上都加了相似-XX:PermSize=256M -XX:MaxPermSize=256M的參數,升級到JDK8以後,由於Perm已經沒了,若是還有這些參數JVM會拋出一些警告信息,因而咱們會將參數進行升級,好比直接將PermSize改爲MetaspaceSizeMaxPermSize改爲MaxMetaspaceSize,可是咱們後面會發現一個問題,常常會看到MetaspaceOutOfMemory異常或者GC日誌裏提示Metaspace致使的Full GC,此時咱們不得不將MaxMetaspaceSize以及MetaspaceSize調大到512M或者更大,幸運的話,發現問題解決了,後面沒再出現OOM,可是有時候也會很不幸,仍然會出現OOM。此時你們是否是很是疑惑了,代碼徹底沒有變化,可是加載類貌似須要更多的內存?性能優化

以前我其實並無仔細去想這個問題,碰到這類OOM的問題,都以爲主要是Metaspace內存碎片的問題,由於以前幫人解決過相似的問題,他們構建了成千上萬個類加載器,確實也是由於Metsapce碎片的問題致使的,由於Metaspace並不會作壓縮,解決的方案主要是調大MetaspaceSizeMaxMetaspaceSize,並將它們設置相等。而後此次碰到的問題並非這樣,類加載個數並很少,然而卻拋出了Metaspace的OutOfMemory異常,而且Full GC一直持續着,並且從jstat來看,Metaspace的GC先後使用狀況基本不變,也就是GC先後基本沒有回收什麼內存。網絡

經過咱們的內存分析工具看到的現象是同一個類加載器竟然加載了同一個類多遍,內存裏有多份類實例,這個咱們能夠經過加上-verbose:class的參數也能獲得驗證,要輸出以下日誌,那只有在不判定義某個類纔會輸出,因而想構建出這種場景來,因而簡單地寫了個demo來驗證
image.png工具

Demo

image.png
代碼很簡單,就是經過反射直接調用ClassLoader的defineClass方法來對某個類作重複的定義。
其中在JDK7下跑的JVM參數設置的是:
image.png
在JDK8下跑的JVM參數是:
image.png
你們能夠經過jstat -gcutil <pid> 1000看看JDK7和JDK8下有什麼不同,結果你會發現JDK7下Perm的使用率隨着FGC的進行GC先後不斷髮生着變化,而Metsapce的使用率到必定階段以後GC先後卻一直沒有變化性能

JDK7下的結果:
image.png
JDK8下的結果:
image.png學習

重複類定義

重複類定義,從上面的Demo裏已經獲得了證實,當咱們屢次調用ClassLoader的defineClass方法的時候哪怕是同一個類加載器加載同一個類文件,在JVM裏也會在對應的Perm或者Metaspace裏建立多份Klass結構,固然通常狀況下咱們不會直接這麼調用,可是反射提供了這麼強大的能力,有些人仍是會利用這種寫法,其實我想直接這麼用的人對類加載的實現機制真的沒有全弄明白,包括此次問題發生的場景其實仍是吸納進JDK裏的jaxp/jaxws,好比它就存在這樣的代碼實現com.sun.xml.bind.v2.runtime.reflect.opt.Injector裏的inject方法就存在直接調用的狀況:
image.png
不過從2.2.2這個版本開始這種實現就改變了
image.png
因此你們若是仍是使用jaxb-impl-2.2.2如下版本的請注意啦,升級到JDK8可能會存在本文說的問題。測試

重複類定義帶來的影響

那重複類定義會帶來什麼危害呢?正常的類加載都會先走一遍緩存查找,看是否已經有了對應的類,若是有了就直接返回,若是沒有就進行定義,若是直接調用類定義的方法,在JVM裏會建立多份臨時的類結構實例,這些相關的結構是存在Perm或者Metaspace裏的,也就是說會消耗Perm或Metaspace的內存,可是這些類在定義出來以後,最終會作一次約束檢查,若是發現已經定義了,那就直接拋出LinkageError的異常
image.png
這樣這些臨時建立的結構,只能等待GC的時候去回收掉了,由於它們不可達,因此在GC的時候會被回收,那問題來了,爲何在Perm下能正常回收,可是在Metaspace裏不能正常回收呢?優化

Perm和Metaspace在類卸載上的差別

這裏我主要拿咱們目前最經常使用的GC算法CMS GC舉例。spa

在JDK7 CMS下,Perm的結構其實和Old的內存結構是同樣的,若是Perm不夠的時候咱們會作一次Full GC,這個Full GC默認狀況下是會對各個分代作壓縮的,包括Perm,這樣一來根據對象的可達性,任何一個類都只會和一個活着的類加載器綁定,在標記階段將這些類標記成活的,並將他們進行新地址的計算及移動壓縮,而以前由於重複定義生成的類結構等,由於沒有將它們和任何一個活着的類加載器關聯(有個叫作SystemDictionary的Hashtable結構來記錄這種關聯),從而在壓縮過程當中會被回收掉。
image.png
在JDK8下,Metaspace是徹底獨立分散的內存結構,由非連續的內存組合起來,在Metaspace達到了觸發GC的閾值的時候(和MaxMetaspaceSize及MetaspaceSize有關),就會作一次Full GC,可是此次Full GC,並不會對Metaspace作壓縮,惟一卸載類的狀況是,對應的類加載器必須是死的,若是類加載器都是活的,那確定不會作卸載的事情了
image.png
從上面貼的代碼咱們也能看出來,JDK7裏會對Perm作壓縮,而後JDK8裏並不會對Metaspace作壓縮,從而只要和那些重複定義的類相關的類加載一直存活,那將一直不會被回收,可是若是類加載死了,那就會被回收,這是由於那些重複類都是在和這個類加載器關聯的內存塊裏分配的,若是這個類加載器死了,那整塊內存會被清理並被下次重用。

如何證實壓縮能回收Perm裏的重複類

在沒看GC源碼的狀況下,有什麼辦法來證實Perm在FGC下的回收是由於壓縮而致使那些重複類被回收呢?你們能夠改改上面的測試用例,將最後那個死循環改一下:
image.png
在System.gc那裏設置個斷點,而後再經過jstat -gcutil <pid> 1000來看Perm的使用率是否發生變化,另外你再加上-XX:+ ExplicitGCInvokesConcurrent再重複上面的動做,你看看輸出是怎樣的,爲何這個能夠證實,你們能夠想想,哈哈

一塊兒來學習吧

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

記一次 Java 服務性能優化

相關文章
相關標籤/搜索