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

概述

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

Demo

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

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

public class AAB {}

接著我們創(chuàng)建一個類加載TestLoader,里面存一個WeakHashMap,專門來存TestLoader的,并且復(fù)寫loadClass方法,如果是加載AAB這個類,就創(chuàng)建一個新的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;
        }
}

再看我們的主類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 {
            //創(chuàng)建一個新的類加載器,從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();
        }
    }
}

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

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

JDK8里的metaspace

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

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

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

這塊具體的情況后面可以專門寫一篇文章來介紹,包括內(nèi)存結(jié)構(gòu),內(nèi)存分配,GC等。

JDK8里的ClassLoaderDataGraph

每個類加載器都會對應(yīng)一個ClassLoaderData的數(shù)據(jù)結(jié)構(gòu),里面會存譬如具體的類加載器對象,加載的klass,管理內(nèi)存的metaspace等,它是一個鏈?zhǔn)浇Y(jié)構(gòu),會鏈到下一個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;

  ...

}

這里提幾個屬性:

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

再來看下構(gòu)造函數(shù):

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屬性的值是根據(jù)_is_anonymous以及當(dāng)前類加載器是不是bootstrapClassLoader來的。

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

void ClassLoaderDataGraph::roots_cld_do(CLDClosure* strong, CLDClosure* weak) {
  //從最后一個創(chuàng)建的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);
    }
  }

類加載器什么時候被回收

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

從各種GC情況來看:

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

僵尸類加載器如何產(chǎn)生

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

lambda匿名類加載

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

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
}

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

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);

...
}

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

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

類加載器依賴導(dǎo)致的

這里說的類加載器依賴,并不是說ClassLoader里的parent建立的那種依賴關(guān)系,如果是這種關(guān)系,那其實通過mat或者zprofiler這樣的工具都是可以分析出來的,但是還存在一種情況,那些工具都是分析不出來的,這種關(guān)系就是通過ClassLoaderData里的_dependencies屬性得出來的,比如說如果A類加載器的_dependencies屬性里記錄了B類加載器,那當(dāng)GC遍歷A類加載器的時候也會遍歷B類加載器,并將其標(biāo)活,哪怕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);
  }
}

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

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

個人微信公眾號

gzh.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容