假笨说-谨防JDK8重复类定义造成的内存泄漏
概述
如今JDK8成了主流,大家都緊鑼密鼓地進(jìn)行著升級(jí),享受著JDK8帶來(lái)的各種便利,然而有時(shí)候升級(jí)并沒(méi)有那么順利?比如說(shuō)今天要說(shuō)的這個(gè)問(wèn)題。我們都知道JDK8在內(nèi)存模型上最大的改變是,放棄了Perm,迎來(lái)了Metaspace的時(shí)代。如果你對(duì)Metaspace還不熟,之前我寫(xiě)過(guò)一篇介紹Metaspace的文章,大家有興趣的可以看看我前面的那篇文章。
我們之前一般在系統(tǒng)的JVM參數(shù)上都加了類(lèi)似-XX:PermSize=256M -XX:MaxPermSize=256M的參數(shù),升級(jí)到JDK8之后,因?yàn)镻erm已經(jīng)沒(méi)了,如果還有這些參數(shù)JVM會(huì)拋出一些警告信息,于是我們會(huì)將參數(shù)進(jìn)行升級(jí),比如直接將PermSize改成MetaspaceSize,MaxPermSize改成MaxMetaspaceSize,但是我們后面會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,經(jīng)常會(huì)看到Metaspace的OutOfMemory異常或者GC日志里提示Metaspace導(dǎo)致的Full GC,此時(shí)我們不得不將MaxMetaspaceSize以及MetaspaceSize調(diào)大到512M或者更大,幸運(yùn)的話,發(fā)現(xiàn)問(wèn)題解決了,后面沒(méi)再出現(xiàn)OOM,但是有時(shí)候也會(huì)很不幸,仍然會(huì)出現(xiàn)OOM。此時(shí)大家是不是非常疑惑了,代碼完全沒(méi)有變化,但是加載類(lèi)貌似需要更多的內(nèi)存?
之前我其實(shí)并沒(méi)有仔細(xì)去想這個(gè)問(wèn)題,碰到這類(lèi)OOM的問(wèn)題,都覺(jué)得主要是Metaspace內(nèi)存碎片的問(wèn)題,因?yàn)橹皫腿私鉀Q過(guò)類(lèi)似的問(wèn)題,他們構(gòu)建了成千上萬(wàn)個(gè)類(lèi)加載器,確實(shí)也是因?yàn)镸etsapce碎片的問(wèn)題導(dǎo)致的,因?yàn)镸etaspace并不會(huì)做壓縮,解決的方案主要是調(diào)大MetaspaceSize和MaxMetaspaceSize,并將它們?cè)O(shè)置相等。然后這次碰到的問(wèn)題并不是這樣,類(lèi)加載個(gè)數(shù)并不多,然而卻拋出了Metaspace的OutOfMemory異常,并且Full GC一直持續(xù)著,而且從jstat來(lái)看,Metaspace的GC前后使用情況基本不變,也就是GC前后基本沒(méi)有回收什么內(nèi)存。
通過(guò)我們的內(nèi)存分析工具看到的現(xiàn)象是同一個(gè)類(lèi)加載器居然加載了同一個(gè)類(lèi)多遍,內(nèi)存里有多份類(lèi)實(shí)例,這個(gè)我們可以通過(guò)加上-verbose:class的參數(shù)也能得到驗(yàn)證,要輸出如下日志,那只有在不斷定義某個(gè)類(lèi)才會(huì)輸出,于是想構(gòu)建出這種場(chǎng)景來(lái),于是簡(jiǎn)單地寫(xiě)了個(gè)demo來(lái)驗(yàn)證
[Loaded ResponseVO$JaxbAccessorM_getDescription_setDescription_java_lang_String from __JVM_DefineClass__] [Loaded ResponseVO$JaxbAccessorM_getDescription_setDescription_java_lang_String from __JVM_DefineClass__] [Loaded ResponseVO$JaxbAccessorM_getDescription_setDescription_java_lang_String from __JVM_DefineClass__]Demo
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Method;/*** Created by nijiaben on 2017/3/7.*/ public class B {public static void main(String args[]) throws Throwable {Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{String.class, byte[].class, int.class, int.class});defineClass.setAccessible(true);File file = new File("/Users/nijiaben/BBBB.class");byte[] bcs = new byte[(int) file.length()];FileInputStream in = null;try {in = new FileInputStream(file);while ((in.read(bcs)) != -1) {}} catch (Exception e) {} finally {if (in != null) {try {in.close();} catch (IOException e) {}}}while (true) {try {defineClass.invoke(B.class.getClassLoader(), new Object[]{"BBBB", bcs, 0, bcs.length});} catch (Throwable e) {}}} }代碼很簡(jiǎn)單,就是通過(guò)反射直接調(diào)用ClassLoader的defineClass方法來(lái)對(duì)某個(gè)類(lèi)做重復(fù)的定義。
其中在JDK7下跑的JVM參數(shù)設(shè)置的是:
-Xmx100M -Xms100M -verbose:class -XX:+PrintGCDetails -XX:MaxPermSize=50M -XX:PermSize=50M -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled在JDK8下跑的JVM參數(shù)是:
-Xmx100M -Xms100M -verbose:class -XX:+PrintGCDetails -XX:MaxMetaspaceSize=50M -XX:MetaspaceSize=50M -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled二者區(qū)別就在于一個(gè)是PermSize,一個(gè)是MetaspaceSize
大家可以通過(guò)jstat -gcutil 1000看看JDK7和JDK8下有什么不一樣,結(jié)果你會(huì)發(fā)現(xiàn)JDK7下Perm的使用率隨著FGC的進(jìn)行GC前后不斷發(fā)生著變化,而Metsapce的使用率到一定階段之后GC前后卻一直沒(méi)有變化
JDK7下的結(jié)果:
[Full GC[CMS: 0K->346K(68288K), 0.0267620 secs] 12607K->346K(99008K), [CMS Perm : 51199K->3122K(51200K)], 0.0269490 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]JDK8下的結(jié)果:
[Full GC (Metadata GC Threshold) [CMS: 5308K->5308K(68288K), 0.0397720 secs] 5844K->5308K(99008K), [Metaspace: 49585K->49585K(1081344K)], 0.0398189 secs] [Times: user=0.04 sys=0.00, real=0.04 secs] [Full GC (Last ditch collection) [CMS: 5308K->5308K(68288K), 0.0343949 secs] 5308K->5308K(99008K), [Metaspace: 49585K->49585K(1081344K)], 0.0344473 secs] [Times: user=0.03 sys=0重復(fù)類(lèi)定義
重復(fù)類(lèi)定義,從上面的Demo里已經(jīng)得到了證明,當(dāng)我們多次調(diào)用ClassLoader的defineClass方法的時(shí)候哪怕是同一個(gè)類(lèi)加載器加載同一個(gè)類(lèi)文件,在JVM里也會(huì)在對(duì)應(yīng)的Perm或者M(jìn)etaspace里創(chuàng)建多份Klass結(jié)構(gòu),當(dāng)然一般情況下我們不會(huì)直接這么調(diào)用,但是反射提供了這么強(qiáng)大的能力,有些人還是會(huì)利用這種寫(xiě)法,其實(shí)我想直接這么用的人對(duì)類(lèi)加載的實(shí)現(xiàn)機(jī)制真的沒(méi)有全弄明白,包括這次問(wèn)題發(fā)生的場(chǎng)景其實(shí)還是吸納進(jìn)JDK里的jaxp/jaxws,比如它就存在這樣的代碼實(shí)現(xiàn)com.sun.xml.bind.v2.runtime.reflect.opt.Injector里的inject方法就存在直接調(diào)用的情況:
private synchronized Class inject(String className, byte[] image){if (!this.loadable) {return null;}Class c = (Class)this.classes.get(className);if (c == null){try{c = (Class)defineClass.invoke(this.parent, new Object[] { className.replace('/', '.'), image, Integer.valueOf(0), Integer.valueOf(image.length) });resolveClass.invoke(this.parent, new Object[] { c });}catch (IllegalAccessException e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}catch (InvocationTargetException e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}catch (SecurityException e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}catch (LinkageError e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}this.classes.put(className, c);}return c;}不過(guò)從2.2.2這個(gè)版本開(kāi)始這種實(shí)現(xiàn)就改變了
private Class inject(String className, byte[] image){...c = (Class)findLoadedClass.invoke(this.parent, new Object[] { className.replace('/', '.') });...if (c == null){c = (Class)defineClass.invoke(this.parent, new Object[] { className.replace('/', '.'), image, Integer.valueOf(0), Integer.valueOf(image.length) });resolveClass.invoke(this.parent, new Object[] { c })...}}所以大家如果還是使用jaxb-impl-2.2.2以下版本的請(qǐng)注意啦,升級(jí)到JDK8可能會(huì)存在本文說(shuō)的問(wèn)題。
重復(fù)類(lèi)定義帶來(lái)的影響
那重復(fù)類(lèi)定義會(huì)帶來(lái)什么危害呢?正常的類(lèi)加載都會(huì)先走一遍緩存查找,看是否已經(jīng)有了對(duì)應(yīng)的類(lèi),如果有了就直接返回,如果沒(méi)有就進(jìn)行定義,如果直接調(diào)用類(lèi)定義的方法,在JVM里會(huì)創(chuàng)建多份臨時(shí)的類(lèi)結(jié)構(gòu)實(shí)例,這些相關(guān)的結(jié)構(gòu)是存在Perm或者M(jìn)etaspace里的,也就是說(shuō)會(huì)消耗Perm或Metaspace的內(nèi)存,但是這些類(lèi)在定義出來(lái)之后,最終會(huì)做一次約束檢查,如果發(fā)現(xiàn)已經(jīng)定義了,那就直接拋出LinkageError的異常
void SystemDictionary::check_constraints(int d_index, unsigned int d_hash,instanceKlassHandle k,Handle class_loader, bool defining,TRAPS) {const char *linkage_error = NULL;{Symbol* name = k->name();ClassLoaderData *loader_data = class_loader_data(class_loader);MutexLocker mu(SystemDictionary_lock, THREAD);Klass* check = find_class(d_index, d_hash, name, loader_data);if (check != (Klass*)NULL) {// if different InstanceKlass - duplicate class definition,// else - ok, class loaded by a different thread in parallel,// we should only have found it if it was done loading and ok to use// system dictionary only holds instance classes, placeholders// also holds array classesassert(check->oop_is_instance(), "noninstance in systemdictionary");if ((defining == true) || (k() != check)) {linkage_error = "loader (instance of %s): attempted duplicate class ""definition for name: \"%s\"";} else {return;}}...}這樣這些臨時(shí)創(chuàng)建的結(jié)構(gòu),只能等待GC的時(shí)候去回收掉了,因?yàn)樗鼈儾豢蛇_(dá),所以在GC的時(shí)候會(huì)被回收,那問(wèn)題來(lái)了,為什么在Perm下能正常回收,但是在Metaspace里不能正常回收呢?
Perm和Metaspace在類(lèi)卸載上的差異
這里我主要拿我們目前最常用的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ì)對(duì)各個(gè)分代做壓縮的,包括Perm,這樣一來(lái)根據(jù)對(duì)象的可達(dá)性,任何一個(gè)類(lèi)都只會(huì)和一個(gè)活著的類(lèi)加載器綁定,在標(biāo)記階段將這些類(lèi)標(biāo)記成活的,并將他們進(jìn)行新地址的計(jì)算及移動(dòng)壓縮,而之前因?yàn)橹貜?fù)定義生成的類(lèi)結(jié)構(gòu)等,因?yàn)闆](méi)有將它們和任何一個(gè)活著的類(lèi)加載器關(guān)聯(lián)(有個(gè)叫做SystemDictionary的Hashtable結(jié)構(gòu)來(lái)記錄這種關(guān)聯(lián)),從而在壓縮過(guò)程中會(huì)被回收掉。
void GenMarkSweep::mark_sweep_phase4() {// All pointers are now adjusted, move objects accordingly// It is imperative that we traverse perm_gen first in phase4. All// classes must be allocated earlier than their instances, and traversing// perm_gen first makes sure that all klassOops have moved to their new// location before any instance does a dispatch through it's klass!// The ValidateMarkSweep live oops tracking expects us to traverse spaces// in the same order in phase2, phase3 and phase4. We don't quite do that// here (perm_gen first rather than last), so we tell the validate code// to use a higher index (saved from phase2) when verifying perm_gen.GenCollectedHeap* gch = GenCollectedHeap::heap();Generation* pg = gch->perm_gen();GCTraceTime tm("phase 4", PrintGC && Verbose, true, _gc_timer);trace("4");VALIDATE_MARK_SWEEP_ONLY(reset_live_oop_tracking(true));pg->compact();VALIDATE_MARK_SWEEP_ONLY(reset_live_oop_tracking(false));GenCompactClosure blk;gch->generation_iterate(&blk, true);VALIDATE_MARK_SWEEP_ONLY(compaction_complete());pg->post_compact(); // Shared spaces verification. }在JDK8下,Metaspace是完全獨(dú)立分散的內(nèi)存結(jié)構(gòu),由非連續(xù)的內(nèi)存組合起來(lái),在Metaspace達(dá)到了觸發(fā)GC的閾值的時(shí)候(和MaxMetaspaceSize及MetaspaceSize有關(guān)),就會(huì)做一次Full GC,但是這次Full GC,并不會(huì)對(duì)Metaspace做壓縮,唯一卸載類(lèi)的情況是,對(duì)應(yīng)的類(lèi)加載器必須是死的,如果類(lèi)加載器都是活的,那肯定不會(huì)做卸載的事情了
void GenMarkSweep::mark_sweep_phase4() {// All pointers are now adjusted, move objects accordingly// It is imperative that we traverse perm_gen first in phase4. All// classes must be allocated earlier than their instances, and traversing// perm_gen first makes sure that all Klass*s have moved to their new// location before any instance does a dispatch through it's klass!// The ValidateMarkSweep live oops tracking expects us to traverse spaces// in the same order in phase2, phase3 and phase4. We don't quite do that// here (perm_gen first rather than last), so we tell the validate code// to use a higher index (saved from phase2) when verifying perm_gen.GenCollectedHeap* gch = GenCollectedHeap::heap();GCTraceTime tm("phase 4", PrintGC && (Verbose || LogCMSParallelFullGC),true, _gc_timer, _gc_tracer->gc_id());trace("4");GenCompactClosure blk;gch->generation_iterate(&blk, true); }從上面貼的代碼我們也能看出來(lái),JDK7里會(huì)對(duì)Perm做壓縮,然后JDK8里并不會(huì)對(duì)Metaspace做壓縮,從而只要和那些重復(fù)定義的類(lèi)相關(guān)的類(lèi)加載一直存活,那將一直不會(huì)被回收,但是如果類(lèi)加載死了,那就會(huì)被回收,這是因?yàn)槟切┲貜?fù)類(lèi)都是在和這個(gè)類(lèi)加載器關(guān)聯(lián)的內(nèi)存塊里分配的,如果這個(gè)類(lèi)加載器死了,那整塊內(nèi)存會(huì)被清理并被下次重用。
如何證明壓縮能回收Perm里的重復(fù)類(lèi)
在沒(méi)看GC源碼的情況下,有什么辦法來(lái)證明Perm在FGC下的回收是因?yàn)閴嚎s而導(dǎo)致那些重復(fù)類(lèi)被回收呢?大家可以改改上面的測(cè)試用例,將最后那個(gè)死循環(huán)改一下:
int i = 0;while (i++ < 1000) {try {defineClass.invoke(B.class.getClassLoader(), new Object[]{"BBBB", bcs, 0, bcs.length});} catch (Throwable e) {}}System.gc();在System.gc那里設(shè)置個(gè)斷點(diǎn),然后再通過(guò)jstat -gcutil 1000來(lái)看Perm的使用率是否發(fā)生變化,另外你再加上-XX:+ ExplicitGCInvokesConcurrent再重復(fù)上面的動(dòng)作,你看看輸出是怎樣的,為什么這個(gè)可以證明,大家可以想一想,哈哈
轉(zhuǎn)載自: 微信公眾號(hào)你假笨
原文鏈接:https://zhuanlan.zhihu.com/p/25634935
總結(jié)
以上是生活随笔為你收集整理的假笨说-谨防JDK8重复类定义造成的内存泄漏的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 冷静对待你遇到的所有Java内存异常
- 下一篇: 你应该知道的缓存进化史