JVM源碼分析之JDK8下的僵尸(無法回收)類加載器

qbom0898 8年前發布 | 18K 次閱讀 JDK JVM 源碼分析 Java開發 Java8

概述

這篇文章基于最近在排查的一個問題,花了我們團隊不少時間來排查這個問題,現象是有一些類加載器是作為key放到WeakHashMap里的,但是經歷過多次full gc之后,依然堅挺地存在內存里,但是從代碼上來說這些類加載器是應該被回收的,因為沒有任何強引用可以到達這些類加載器了,于是我們做了內存dump,分析了下內存,發現除了一個WeakHashMap外并沒有別的GC ROOT途徑達到這些類加載器了,那這樣一來經過多次FULL GC肯定是可以被回收的,但是事實卻不是這樣,為了讓這個問題聽起來更好理解,還是照例先上個Demo,完全模擬了這種場景。

Demo

首先我們創建兩個類AAA和AAB,分別打包到兩個不同jar里,比如AAA.jar和AAB.jar,這兩個類之間是有關系的,AAA里有個屬性是AAB類型的,注意這兩個jar不要放到classpath里讓appClassLoader加載到:

public class AAA {
        private AAB aab;
        public AAA(){
                aab=new AAB();
        }
        public void clear(){
                aab=null;
        }
}

public class AAB {}</code></pre>

接著我們創建一個類加載TestLoader,里面存一個WeakHashMap,專門來存TestLoader的,并且復寫loadClass方法,如果是加載AAB這個類,就創建一個新的TestLoader來從AAB.jar里加載這個類

import java.net.URL;
import java.net.URLClassLoader;
import java.util.WeakHashMap;

public class TestLoader extends URLClassLoader { public static WeakHashMap<TestLoader,Object> map=new WeakHashMap<TestLoader,Object>(); private static int count=0; public TestLoader(URL[] urls){ super(urls); map.put(this, new Object()); } @SuppressWarnings("resource") public Class<?> loadClass(String name) throws ClassNotFoundException { if(name.equals("AAB") && count==0){ try { count=1; URL[] urls = new URL[1]; urls[0] = new URL("file:///home/nijiaben/tmp/AAB.jar"); return new TestLoader(urls).loadClass("AAB"); }catch (Exception e){ e.printStackTrace(); } }else{ return super.loadClass(name); } return null; } }</code></pre>

再看我們的主類TTest,一些說明都寫在類里了:

import java.lang.reflect.Method;
import java.net.URL;

/**

  • Created by nijiaben on 4/22/16. */ public class TTest { private Object aaa; public static void main(String args[]){

     try {
         TTest tt = new TTest();
         //將對象移到old,并置空aaa的aab屬性
         test(tt);
         //清理掉aab對象
         System.gc();
         System.out.println("finished");
     }catch (Exception e){
         e.printStackTrace();
     }
    

    }

    @SuppressWarnings("resource")

     public static void test(TTest tt){
     try {
       //創建一個新的類加載器,從AAA.jar里加載AAA類
         URL[] urls = new URL[1];
         urls[0] = new URL("file:///home/nijiaben/tmp/AAA.jar");
         tt.aaa=new TestLoader(urls).loadClass("AAA").newInstance();
         //保證類加載器對象能進入到old里,因為ygc是不會對classLoader做清理的
         for(int i=0;i<10;i++){
             System.gc();
             Thread.sleep(1000);
         }
         //將aaa里的aab屬性清空掉,以便在后面gc的時候能清理掉aab對象,這樣AAB的類加載器其實就沒有什么地方有強引用了,在full gc的時候能被回收
         Method[] methods=tt.aaa.getClass().getDeclaredMethods();
         for(Method m:methods){
             if(m.getName().equals("clear")){
                     m.invoke(tt.aaa);
                     break;
             }
         }
     }catch (Exception e){
         e.printStackTrace();
     }
    

    } }</code></pre>

    運行的時候請跑在JDK8下,打個斷點在 System.out.println("finished") 的地方,然后做一次內存dump。

    從上面的例子中我們得知,TTest是類加載器AppClassLoader加載的,其屬性aaa的對象類型是通過TestLoader從AAA.jar里加載的,而aaa里的aab屬性是從一個全新的類加載器TestLoader從AAB.jar里加載的,當我們做了多次System GC之后,這些對象會移到old,在做最后一次GC之后,aab對象會從內存里移除,其類加載器此時已經是沒有任何地方的強引用了,只有一個WeakHashMap引用它,理論上做GC的時候也應該被回收,但是事實時這個AAB的這個類加載器并沒有被回收,從分析結果來看,GC ROOT路徑是WeakHashMap,如圖所示:

    JVM源碼分析之JDK8下的僵尸(無法回收)類加載器

    JDK8里的metaspace

    這里不得不提的一個概念是JDK8里的metaspace,它是為了取代perm的,至于好處是什么,我個人覺得不是那么明顯,有點費力不討好的感覺,代碼改了很多,但是實際收益并不明顯,據說是oracle內部斗爭的一個結果。

    在JDK8里雖然沒了perm,但是klass的信息還是要有地方存,jvm里為此分配了兩塊內存,一塊是緊挨著heap來的,就和perm一樣,專門用來存klass的信息,可以通過 -XX:CompressedClassSpaceSize 來設置大小,另外一塊和它們不一定連著,主要是存非klass之外的其他信息,比如常量池什么的,可以通過 -XX:InitialBootClassLoaderMetaspaceSize 來設置,同時我們還可以通過 -XX:MaxMetaspaceSize 來設置觸發metaspace回收的閾值。

    每個類加載器都會從全局的metaspace空間里取一些metaChunk管理起來,當有類定義的時候,其實就是從這些內存里分配的,當不夠的時候再去全局的metaspace里分配一塊并管理起來。

    這塊具體的情況后面可以專門寫一篇文章來介紹,包括內存結構,內存分配,GC等。

    JDK8里的ClassLoaderDataGraph

    每個類加載器都會對應一個ClassLoaderData的數據結構,里面會存譬如具體的類加載器對象,加載的klass,管理內存的metaspace等,它是一個鏈式結構,會鏈到下一個ClassLoaderData上,gc的時候通過ClassLoaderDataGraph來遍歷這些ClassLoaderData,ClassLoaderDataGraph的第一個ClassLoaderData是bootstrapClassLoader的

    class ClassLoaderData : public CHeapObj<mtClass> {
    ...
    static ClassLoaderData * _the_null_class_loader_data;

    oop _class_loader; // oop used to uniquely identify a class loader

                           // class loader or a canonical class path
    

    Dependencies _dependencies; // holds dependencies from this class loader

                           // data to others.
    
    

    Metaspace * _metaspace; // Meta-space where meta-data defined by the

                        // classes in the class loader are allocated.
    

    Mutex* _metaspace_lock; // Locks the metaspace for allocations and setup. bool _unloading; // true if this class loader goes away bool _keep_alive; // if this CLD is kept alive without a keep_alive_object(). bool _is_anonymous; // if this CLD is for an anonymous class volatile int _claimed; // true if claimed, for example during GC traces.

                        // To avoid applying oop closure more than once.
                        // Has to be an int because we cas it.
    

    Klass* _klasses; // The classes defined by the class loader.

    JNIHandleBlock* _handles; // Handles to constant pool arrays

    // These method IDs are created for the class loader and set to NULL when the // class loader is unloaded. They are rarely freed, only for redefine classes // and if they lose a data race in InstanceKlass. JNIMethodBlock* _jmethod_ids;

    // Metadata to be deallocated when it's safe at class unloading, when // this class loader isn't unloaded itself. GrowableArray<Metadata> _deallocate_list;

    // Support for walking class loader data objects ClassLoaderData* _next; /// Next loader_datas created

    // ReadOnly and ReadWrite metaspaces (static because only on the null // class loader for now). static Metaspace _ro_metaspace; static Metaspace _rw_metaspace;

    ...

}</code></pre>

這里提幾個屬性:

  • _class_loader : 就是對應的類加載器對象
  • _keep_alive : 如果這個值是true,那這個類加載器會認為是活的,會將其做為GC ROOT的一部分,gc的時候不會被回收
  • _unloading : 表示這個類加載是否需要卸載的
  • _is_anonymous : 是否匿名,這種ClassLoaderData主要是在lambda表達式里用的,這個我后面會詳細說
  • _next : 指向下一個ClassLoaderData,在gc的時候方便遍歷
  • _dependencies : 這個屬性也是本文的重點,后面會細說

再來看下構造函數:

ClassLoaderData::ClassLoaderData(Handle h_class_loader, bool is_anonymous, Dependencies dependencies) :
  _class_loader(h_class_loader()),
  _is_anonymous(is_anonymous),
  // An anonymous class loader data doesn't have anything to keep
  // it from being unloaded during parsing of the anonymous class.
  // The null-class-loader should always be kept alive.
  _keep_alive(is_anonymous || h_class_loader.is_null()),
  _metaspace(NULL), _unloading(false), _klasses(NULL),
  _claimed(0), _jmethod_ids(NULL), _handles(NULL), _deallocate_list(NULL),
  _next(NULL), _dependencies(dependencies),
  _metaspace_lock(new Mutex(Monitor::leaf+1, "Metaspace allocation lock", true)) {
    // empty
}

可見, _keep_ailve 屬性的值是根據 _is_anonymous 以及當前類加載器是不是bootstrapClassLoader來的。

_keep_alive 到底用在哪?其實是在GC的的時候,來決定要不要用Closure或者用什么Closure來掃描對應的ClassLoaderData。

void ClassLoaderDataGraph::roots_cld_do(CLDClosure* strong, CLDClosure* weak) {
  //從最后一個創建的classloader到bootstrapClassloader  
  for (ClassLoaderData* cld = _head;  cld != NULL; cld = cld->_next) {
    //如果是ygc,那weak和strong是一樣的,對所有的類加載器都做掃描,保證它們都是活的 
    //如果是cms initmark階段,如果要unload_classes了(should_unload_classes()返回true),則weak為null,那就只遍歷bootstrapclassloader以及正在做匿名類加載的類加載  
    CLDClosure* closure = cld->keep_alive() ? strong : weak;
    if (closure != NULL) {
      closure->do_cld(cld);
    }
  }

類加載器什么時候被回收

類加載器是否需要被回收,其實就是看這個類加載器對象是否是活的,所謂活的就是這個類加載器加載的任何一個類或者這些類的對象是強可達的,當然還包括這個類加載器本身就是GC ROOT一部分或者有GC ROOT可達的路徑,那這個類加載器就肯定不會被回收。

從各種GC情況來看:

  • 如果是YGC,類加載器是作為GC ROOT的,也就是都不會被回收
  • 如果是Full GC,只要是死的就會被回收
  • 如果是CMS GC,CMS GC過程也是會做標記的(這是默認情況,不過可以通過一些參數來改變),但是不會做真正的清理,真正的清理動作是發生在下次進入安全點的時候。

僵尸類加載器如何產生

如果類加載器是與GC ROOT的對象存在真正依賴的這種關系,這種類加載器對象是活的無可厚非,我們通過zprofiler或者mat都可以分析出來,可以將鏈路繪出來,但是有兩種情況例外:

lambda匿名類加載

lambda匿名類加載走的是unsafe的defineAnonymousClass方法,這個方法在vm里對應的是下面的方法

UNSAFE_ENTRY(jclass, Unsafe_DefineAnonymousClass(JNIEnv *env, jobject unsafe, jclass host_class, jbyteArray data, jobjectArray cp_patches_jh))
{
  instanceKlassHandle anon_klass;
  jobject res_jh = NULL;

UnsafeWrapper("Unsafe_DefineAnonymousClass"); ResourceMark rm(THREAD);

HeapWord* temp_alloc = NULL;

anon_klass = Unsafe_DefineAnonymousClass_impl(env, host_class, data, cp_patches_jh, &temp_alloc, THREAD); if (anon_klass() != NULL) res_jh = JNIHandles::make_local(env, anon_klass->java_mirror());

// try/finally clause: if (temp_alloc != NULL) { FREE_C_HEAP_ARRAY(HeapWord, temp_alloc, mtInternal); }

// The anonymous class loader data has been artificially been kept alive to // this point. The mirror and any instances of this class have to keep // it alive afterwards. if (anon_klass() != NULL) { anon_klass->class_loader_data()->set_keep_alive(false); }

// let caller initialize it as needed...

return (jclass) res_jh; } UNSAFE_END } </code></pre>

可見,在創建成功匿名類之后,會將對應的ClassLoaderData的 _keep_alive 屬性設置為false,那是不是意味著 _keep_alive 屬性在這之前都是true呢?下面的parse_stream 方法是從上面的方法最終會調下來的方法

Klass SystemDictionary::parse_stream(Symbol class_name,
                                      Handle class_loader,
                                      Handle protection_domain,
                                      ClassFileStream st,
                                      KlassHandle host_klass,
                                      GrowableArray<Handle> cp_patches,
                                      TRAPS) {
  TempNewSymbol parsed_name = NULL;

Ticks class_load_start_time = Ticks::now();

ClassLoaderData* loader_data; if (host_klass.not_null()) { // Create a new CLD for anonymous class, that uses the same class loader // as the host_klass assert(EnableInvokeDynamic, ""); guarantee(host_klass->class_loader() == class_loader(), "should be the same"); guarantee(!DumpSharedSpaces, "must not create anonymous classes when dumping"); loader_data = ClassLoaderData::anonymous_class_loader_data(class_loader(), CHECK_NULL); loader_data->record_dependency(host_klass(), CHECK_NULL); } else { loader_data = ClassLoaderData::class_loader_data(class_loader()); }

instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name, loader_data, protection_domain, host_klass, cp_patches, parsed_name, true, THREAD); ...

}

ClassLoaderData* ClassLoaderData::anonymous_class_loader_data(oop loader, TRAPS) { // Add a new class loader data to the graph. return ClassLoaderDataGraph::add(loader, true, CHECK_NULL); }

ClassLoaderData* ClassLoaderDataGraph::add(Handle loader, bool is_anonymous, TRAPS) { // We need to allocate all the oops for the ClassLoaderData before allocating the // actual ClassLoaderData object. ClassLoaderData::Dependencies dependencies(CHECK_NULL);

No_Safepoint_Verifier no_safepoints; // we mustn't GC until we've installed the // ClassLoaderData in the graph since the CLD // contains unhandled oops

ClassLoaderData* cld = new ClassLoaderData(loader, is_anonymous, dependencies);

... }</code></pre>

從上面的代碼得知,只要走了unsafe的那個方法,都會為當前類加載器創建一個ClassLoaderData對象,并設置其 _is_anonymous 為true,也同時意味著_keep_alive 的屬性是true,并加入到ClassLoaderDataGraph中。

試想如果創建的這個匿名類沒有成功,也就是 anon_klass()==null ,那這個_keep_alive 屬性就永遠無法設置為false了,這意味著這個ClassLoaderData對應的ClassLoader對象將永遠都是GC ROOT的一部分,無法被回收,這種情況就是真正的僵尸類加載器了,不過目前我還沒模擬出這種情況來,有興趣的同學可以試一試,如果真的能模擬出來,這絕對是JDK里的一個BUG,可以提交給社區。

類加載器依賴導致的

這里說的類加載器依賴,并不是說ClassLoader里的parent建立的那種依賴關系,如果是這種關系,那其實通過mat或者zprofiler這樣的工具都是可以分析出來的,但是還存在一種情況,那些工具都是分析不出來的,這種關系就是通過ClassLoaderData里的_dependencies 屬性得出來的,比如說如果A類加載器的 _dependencies 屬性里記錄了B類加載器,那當GC遍歷A類加載器的時候也會遍歷B類加載器,并將其標活,哪怕B類加載器其實是可以被回收了的,可以看下下面的代碼

void ClassLoaderData::oops_do(OopClosure f, KlassClosure klass_closure, bool must_claim) {
  if (must_claim && !claim()) {
    return;
  }

f->do_oop(&_class_loader); _dependencies.oops_do(f); _handles->oops_do(f); if (klass_closure != NULL) { classes_do(klass_closure); } }</code></pre>

那問題來了,這種依賴關系是怎么記錄的呢?其實我們上面的demo就模擬了這種情況,可以仔細去看看,我也針對這個demo描述下,比如加載AAA的類加載器TestLoader加載AAA后,并創建AAA對象,此時會看到有個類型是AAB的屬性,此時會對常量池里的類型做一個解析,我們看到TestLoader的loadClass方法的時候做了一個判斷,如果是AAB類型的類加載,那就創建一個新的類加載器對象從AAB.jar里去加載,當加載返回的時候,在jvm里其實就會記錄這么一層依賴關系,認為AAA的類加載器依賴AAB的類加載器,并記錄下來,但是縱觀所有的hotspot代碼,并沒有一個地方來清理這種依賴關系的,也就是說只要這種依賴關系建立起來,會一直持續到AAA的類加載器被回收的時候,AAB的類加載器才會被回收,所以說這算一種偽僵尸類加載器,雖然從依賴關系上其實并不依賴了(比如demo里將AAA的aab屬性做clear清空動作),但是GC會一直認為他們是存在這種依賴關系的,會持續存在一段時間,具體持續多久就看AAA類加載器的情況了。

針對這種情況個人認為需要一個類似引用計數的GC策略,當某兩個類加載器確實沒有任何依賴的時候,將其清理掉這種依賴關系,估計要實現這種改動的地方也挺多,沒那么簡單,所以當時的設計者或許因為這樣并沒有這么做了,我覺得這算是偷懶妥協的結果吧,當然這只是我的一種猜測。

來自: http://lovestblog.cn/blog/2016/04/24/classloader-unload/

 

 本文由用戶 qbom0898 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!