JVM源碼分析之JDK8下的殭屍(沒法回收)類加載器

本文來自: PerfMa技術社區java

PerfMa(笨馬網絡)官網bootstrap

概述

這篇文章基於最近在排查的一個問題,花了咱們團隊很多時間來排查這個問題,現象是有一些類加載器是做爲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 {}
複製代碼

接着咱們建立一個類加載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;
        }
}
複製代碼

再看咱們的主類TTest,一些說明都寫在類裏了:oracle

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();
        }
    }
}
複製代碼

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

從上面的例子中咱們得知,TTest是類加載器AppClassLoader加載的,其屬性aaa的對象類型是經過TestLoader從AAA.jar里加載的,而aaa裏的aab屬性是從一個全新的類加載器TestLoader從AAB.jar里加載的,當咱們作了屢次System GC以後,這些對象會移到old,在作最後一次GC以後,aab對象會從內存裏移除,其類加載器此時已是沒有任何地方的強引用了,只有一個WeakHashMap引用它,理論上作GC的時候也應該被回收,可是事實時這個AAB的這個類加載器並無被回收,從分析結果來看,GC ROOT路徑是WeakHashMap。dom

JDK8裏的metaspace

這裏不得不提的一個概念是JDK8裏的metaspace,它是爲了取代perm的,至於好處是什麼,我我的以爲不是那麼明顯,有點費力不討好的感受,代碼改了不少,可是實際收益並不明顯,聽說是oracle內部鬥爭的一個結果。jvm

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

  ...

}
複製代碼

這裏提幾個屬性:

  • _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
}
複製代碼

可見,在建立成功匿名類以後,會將對應的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);

...
}
複製代碼

從上面的代碼得知,只要走了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);
  }
}
複製代碼

那問題來了,這種依賴關係是怎麼記錄的呢?其實咱們上面的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策略,當某兩個類加載器確實沒有任何依賴的時候,將其清理掉這種依賴關係,估計要實現這種改動的地方也挺多,沒那麼簡單,因此當時的設計者或許由於這樣並無這麼作了,我以爲這算是偷懶妥協的結果吧,固然這只是個人一種猜想。

一塊兒來學習吧

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

基於CAP模型設計企業級真正高可用的分佈式鎖

相關文章
相關標籤/搜索