自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

李嘉鵬:謹(jǐn)防JDK8重復(fù)類定義造成的內(nèi)存泄漏

開發(fā) 開發(fā)工具
如今JDK8成了主流,大家都緊鑼密鼓地進(jìn)行著升級,享受著JDK8帶來的各種便利,然而有時(shí)候升級并沒有那么順利?比如說今天要說的這個(gè)問題。我們都知道JDK8在內(nèi)存模型上最大的改變是,放棄了Perm,迎來了Metaspace的時(shí)代。

概述

如今JDK8成了主流,大家都緊鑼密鼓地進(jìn)行著升級,享受著JDK8帶來的各種便利,然而有時(shí)候升級并沒有那么順利?比如說今天要說的這個(gè)問題。我們都知道JDK8在內(nèi)存模型上***的改變是,放棄了Perm,迎來了Metaspace的時(shí)代。如果你對Metaspace還不熟,之前我寫過一篇介紹Metaspace的文章,大家有興趣的可以看看我前面的那篇文章。

我們之前一般在系統(tǒng)的JVM參數(shù)上都加了類似-XX:PermSize=256M -XX:MaxPermSize=256M的參數(shù),升級到JDK8之后,因?yàn)镻erm已經(jīng)沒了,如果還有這些參數(shù)JVM會(huì)拋出一些警告信息,于是我們會(huì)將參數(shù)進(jìn)行升級,比如直接將PermSize改成MetaspaceSize,MaxPermSize改成MaxMetaspaceSize,但是我們后面會(huì)發(fā)現(xiàn)一個(gè)問題,經(jīng)常會(huì)看到Metaspace的OutOfMemory異常或者GC日志里提示Metaspace導(dǎo)致的Full GC,此時(shí)我們不得不將MaxMetaspaceSize以及MetaspaceSize調(diào)大到512M或者更大,幸運(yùn)的話,發(fā)現(xiàn)問題解決了,后面沒再出現(xiàn)OOM,但是有時(shí)候也會(huì)很不幸,仍然會(huì)出現(xiàn)OOM。此時(shí)大家是不是非常疑惑了,代碼完全沒有變化,但是加載類貌似需要更多的內(nèi)存?

之前我其實(shí)并沒有仔細(xì)去想這個(gè)問題,碰到這類OOM的問題,都覺得主要是Metaspace內(nèi)存碎片的問題,因?yàn)橹皫腿私鉀Q過類似的問題,他們構(gòu)建了成千上萬個(gè)類加載器,確實(shí)也是因?yàn)镸etsapce碎片的問題導(dǎo)致的,因?yàn)镸etaspace并不會(huì)做壓縮,解決的方案主要是調(diào)大MetaspaceSize和MaxMetaspaceSize,并將它們設(shè)置相等。然后這次碰到的問題并不是這樣,類加載個(gè)數(shù)并不多,然而卻拋出了Metaspace的OutOfMemory異常,并且Full GC一直持續(xù)著,而且從jstat來看,Metaspace的GC前后使用情況基本不變,也就是GC前后基本沒有回收什么內(nèi)存。

通過我們的內(nèi)存分析工具看到的現(xiàn)象是同一個(gè)類加載器居然加載了同一個(gè)類多遍,內(nèi)存里有多份類實(shí)例,這個(gè)我們可以通過加上-verbose:class的參數(shù)也能得到驗(yàn)證,要輸出如下日志,那只有在不斷定義某個(gè)類才會(huì)輸出,于是想構(gòu)建出這種場景來,于是簡單地寫了個(gè)demo來驗(yàn)證

Demo

代碼很簡單,就是通過反射直接調(diào)用ClassLoader的defineClass方法來對某個(gè)類做重復(fù)的定義。

其中在JDK7下跑的JVM參數(shù)設(shè)置的是:

在JDK8下跑的JVM參數(shù)是:

大家可以通過jstat -gcutil <pid> 1000看看JDK7和JDK8下有什么不一樣,結(jié)果你會(huì)發(fā)現(xiàn)JDK7下Perm的使用率隨著FGC的進(jìn)行GC前后不斷發(fā)生著變化,而Metsapce的使用率到一定階段之后GC前后卻一直沒有變化

JDK7下的結(jié)果:

JDK8下的結(jié)果:

重復(fù)類定義

重復(fù)類定義,從上面的Demo里已經(jīng)得到了證明,當(dāng)我們多次調(diào)用ClassLoader的defineClass方法的時(shí)候哪怕是同一個(gè)類加載器加載同一個(gè)類文件,在JVM里也會(huì)在對應(yīng)的Perm或者M(jìn)etaspace里創(chuàng)建多份Klass結(jié)構(gòu),當(dāng)然一般情況下我們不會(huì)直接這么調(diào)用,但是反射提供了這么強(qiáng)大的能力,有些人還是會(huì)利用這種寫法,其實(shí)我想直接這么用的人對類加載的實(shí)現(xiàn)機(jī)制真的沒有全弄明白,包括這次問題發(fā)生的場景其實(shí)還是吸納進(jìn)JDK里的jaxp/jaxws,比如它就存在這樣的代碼實(shí)現(xiàn)com.sun.xml.bind.v2.runtime.reflect.opt.Injector里的inject方法就存在直接調(diào)用的情況:

不過從2.2.2這個(gè)版本開始這種實(shí)現(xiàn)就改變了

所以大家如果還是使用jaxb-impl-2.2.2以下版本的請注意啦,升級到JDK8可能會(huì)存在本文說的問題。

重復(fù)類定義帶來的影響

那重復(fù)類定義會(huì)帶來什么危害呢?正常的類加載都會(huì)先走一遍緩存查找,看是否已經(jīng)有了對應(yīng)的類,如果有了就直接返回,如果沒有就進(jìn)行定義,如果直接調(diào)用類定義的方法,在JVM里會(huì)創(chuàng)建多份臨時(shí)的類結(jié)構(gòu)實(shí)例,這些相關(guān)的結(jié)構(gòu)是存在Perm或者M(jìn)etaspace里的,也就是說會(huì)消耗Perm或Metaspace的內(nèi)存,但是這些類在定義出來之后,最終會(huì)做一次約束檢查,如果發(fā)現(xiàn)已經(jīng)定義了,那就直接拋出LinkageError的異常

這樣這些臨時(shí)創(chuàng)建的結(jié)構(gòu),只能等待GC的時(shí)候去回收掉了,因?yàn)樗鼈儾豢蛇_(dá),所以在GC的時(shí)候會(huì)被回收,那問題來了,為什么在Perm下能正?;厥眨窃贛etaspace里不能正?;厥漳?

Perm和Metaspace在類卸載上的差異

這里我主要拿我們目前最常用的GC算法CMS GC舉例。

在JDK7 CMS下,Perm的結(jié)構(gòu)其實(shí)和Old的內(nèi)存結(jié)構(gòu)是一樣的,如果Perm不夠的時(shí)候我們會(huì)做一次Full GC,這個(gè)Full GC默認(rèn)情況下是會(huì)對各個(gè)分代做壓縮的,包括Perm,這樣一來根據(jù)對象的可達(dá)性,任何一個(gè)類都只會(huì)和一個(gè)活著的類加載器綁定,在標(biāo)記階段將這些類標(biāo)記成活的,并將他們進(jìn)行新地址的計(jì)算及移動(dòng)壓縮,而之前因?yàn)橹貜?fù)定義生成的類結(jié)構(gòu)等,因?yàn)闆]有將它們和任何一個(gè)活著的類加載器關(guān)聯(lián)(有個(gè)叫做SystemDictionary的Hashtable結(jié)構(gòu)來記錄這種關(guān)聯(lián)),從而在壓縮過程中會(huì)被回收掉。

在JDK8下,Metaspace是完全獨(dú)立分散的內(nèi)存結(jié)構(gòu),由非連續(xù)的內(nèi)存組合起來,在Metaspace達(dá)到了觸發(fā)GC的閾值的時(shí)候(和MaxMetaspaceSize及MetaspaceSize有關(guān)),就會(huì)做一次Full GC,但是這次Full GC,并不會(huì)對Metaspace做壓縮,唯一卸載類的情況是,對應(yīng)的類加載器必須是死的,如果類加載器都是活的,那肯定不會(huì)做卸載的事情了

從上面貼的代碼我們也能看出來,JDK7里會(huì)對Perm做壓縮,然后JDK8里并不會(huì)對Metaspace做壓縮,從而只要和那些重復(fù)定義的類相關(guān)的類加載一直存活,那將一直不會(huì)被回收,但是如果類加載死了,那就會(huì)被回收,這是因?yàn)槟切┲貜?fù)類都是在和這個(gè)類加載器關(guān)聯(lián)的內(nèi)存塊里分配的,如果這個(gè)類加載器死了,那整塊內(nèi)存會(huì)被清理并被下次重用。

如何證明壓縮能回收Perm里的重復(fù)類

在沒看GC源碼的情況下,有什么辦法來證明Perm在FGC下的回收是因?yàn)閴嚎s而導(dǎo)致那些重復(fù)類被回收呢?大家可以改改上面的測試用例,將***那個(gè)死循環(huán)改一下:

在System.gc那里設(shè)置個(gè)斷點(diǎn),然后再通過jstat -gcutil <pid> 1000來看Perm的使用率是否發(fā)生變化,另外你再加上-XX:+ ExplicitGCInvokesConcurrent再重復(fù)上面的動(dòng)作,你看看輸出是怎樣的,為什么這個(gè)可以證明,大家可以想一想。

【本文是51CTO專欄作者李嘉鵬的原創(chuàng)文章,轉(zhuǎn)載請通過微信公眾號(你假笨,id:lovestblog)聯(lián)系作者本人獲取授權(quán)】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2022-05-09 14:09:23

多線程線程安全

2016-10-31 20:56:57

Javascript閉包內(nèi)存泄漏

2022-03-30 07:32:10

JDK8異步編程

2021-08-07 07:48:28

JDKjava JDK17

2022-05-31 07:32:19

JDK8API工具

2022-04-18 09:54:37

JDK8日期前端

2022-04-21 09:48:54

JDK8JDK7編碼

2022-04-21 07:34:34

JDK8JDK7數(shù)據(jù)

2012-08-13 10:14:36

IBMdW

2011-05-24 16:39:09

Cfree()

2021-01-15 10:03:18

JDK8日期API

2024-04-08 07:27:02

JDK8ZGC垃圾回收

2015-03-30 11:18:50

內(nèi)存管理Android

2019-01-30 18:24:14

Java內(nèi)存泄漏編程語言

2020-01-14 10:57:39

內(nèi)存泄漏虛擬機(jī)

2010-09-03 16:44:22

2010-09-09 08:57:28

2024-03-11 08:22:40

Java內(nèi)存泄漏

2023-12-18 10:45:23

內(nèi)存泄漏計(jì)算機(jī)服務(wù)器

2009-06-16 11:17:49

內(nèi)存泄漏
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號