閱讀258 返回首頁    go 阿裏雲 go 技術社區[雲棲]


JVM源碼分析之自定義類加載器如何拉長YGC

640?wx_fmt=jpeg&tp=webp&wxfrom=5

概述

本文重點講述畢玄大師在其公眾號上發的一個GC問題一個jstack/jmap等不能用的case,對於畢大師那篇文章,題目上沒有提到GC的那個問題,不過進入到文章裏可以看到,既然文章提到了jstack/jmap的問題,這裏也簡單回答下jstack/jmap無法使用的問題,其實最常見的場景是使用jstack/jmap的用戶和目標進程不是同一個用戶,哪怕你執行jstack/jmap的動作是root用戶也無濟於事,不過畢大師這裏主要提到的是jmap -heap/histo這兩個參數帶來的問題,如果使用-heap/histo的參數,其實和大家使用-F參數是一樣的,底層都是通過serviceability agent來實現的,並不是jvm attach的方式,通過sa連上去之後會掛起進程,在serviceability agent裏存在bug可能導致detach的動作不會被執行,從而會讓進程一直掛著,可以通過top命令驗證進程是否處於T狀態,如果是說明進程被掛起了,如果進程被掛起了,可以通過kill -CONT [pid]來恢複。

再回到那個GC的問題,用的參數如下:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms512m -Xmx512m -Xmn100m -XX:+UseConcMarkSweepGC

demo程序如下:

import com.thoughtworks.xstream.XStream;

public class XStreamTest {

    public static void main(String[] args) throws Exception {
        while(true){
            XStream xs = new XStream();
            xs.toString();
            xs = null;
        }
    }

}

執行效果如下

2016-03-14T22:48:01.502+0800: [GC [ParNew: 327680K->4258K(368640K), 0.0179071 secs] 327680K->4258K(1007616K), 0.0179448 secs] [Times: user=0.06 sys=0.01, real=0.01 secs] 
2016-03-14T22:48:05.975+0800: [GC [ParNew: 331938K->10239K(368640K), 0.0336279 secs] 331938K->10239K(1007616K), 0.0336593 secs] [Times: user=0.13 sys=0.02, real=0.03 secs] 
2016-03-14T22:48:12.215+0800: [GC [ParNew: 337919K->14444K(368640K), 0.0471005 secs] 337919K->14444K(1007616K), 0.0471257 secs] [Times: user=0.19 sys=0.02, real=0.05 secs] 
2016-03-14T22:48:21.768+0800: [GC [ParNew: 342124K->19088K(368640K), 0.0605017 secs] 342124K->19088K(1007616K), 0.0605295 secs] [Times: user=0.26 sys=0.03, real=0.06 secs] 
2016-03-14T22:48:35.180+0800: [GC [ParNew: 346768K->20633K(368640K), 0.0993470 secs] 346768K->25248K(1007616K), 0.0993777 secs] [Times: user=0.34 sys=0.04, real=0.09 secs]

發現gc的時間越來越長,但是gc觸發的時機以及回收的效果都差不多,那問題究竟在哪裏呢?

Demo分析

雖然這個demo代碼邏輯很簡單,但是其實這是一個特殊的demo,並不簡單,如果我們將XStream對象換成Object對象,會發現不存在這個問題,既然如此那有必要進去看看這個XStream的構造函數:

 public XStream() {
        this((ReflectionProvider)null, (Mapper)((Mapper)null), (HierarchicalStreamDriver)(new XppDriver()));
    }

    /** @deprecated */
    public XStream(ReflectionProvider reflectionProvider, Mapper mapper, HierarchicalStreamDriver driver) {
        this(reflectionProvider, driver, (ClassLoader)(new CompositeClassLoader()), mapper);
    }

    /** @deprecated */
    public XStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoader classLoader, Mapper mapper) {
        this(reflectionProvider, driver, new ClassLoaderReference(classLoader), mapper, new DefaultConverterLookup());
    }

    public XStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoaderReference classLoader, Mapper mapper, final DefaultConverterLookup defaultConverterLookup) {
        this(reflectionProvider, driver, (ClassLoaderReference)classLoader, mapper, new ConverterLookup() {
            public Converter lookupConverterForType(Class type) {
                return defaultConverterLookup.lookupConverterForType(type);
            }
        }, new ConverterRegistry() {
            public void registerConverter(Converter converter, int priority) {
                defaultConverterLookup.registerConverter(converter, priority);
            }
        });
    }

    /** @deprecated */
    public XStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoader classLoader, Mapper mapper, ConverterLookup converterLookup, ConverterRegistry converterRegistry) {
        this(reflectionProvider, driver, (ClassLoaderReference)(new ClassLoaderReference(classLoader)), mapper, converterLookup, converterRegistry);
    }

    public XStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoaderReference classLoaderReference, Mapper mapper, ConverterLookup converterLookup, ConverterRegistry converterRegistry) {
        if(reflectionProvider == null) {
            reflectionProvider = JVM.newReflectionProvider();
        }

        this.reflectionProvider = reflectionProvider;
        this.hierarchicalStreamDriver = driver;
        this.classLoaderReference = classLoaderReference;
        this.converterLookup = converterLookup;
        this.converterRegistry = converterRegistry;
        this.mapper = mapper == null?this.buildMapper():mapper;
        this.setupMappers();
        this.setupSecurity();
        this.setupAliases();
        this.setupDefaultImplementations();
        this.setupConverters();
        this.setupImmutableTypes();
        this.setMode(1003);
    }

這個構造函數還是很複雜的,裏麵會創建很多的對象,上麵還有一些方法實現我就不貼了,總之都是在不斷構建各種大大小小的對象,一個XStream對象構建出來的時候大概好像有12M的樣子。

那到底是哪些對象會導致ygc不斷增長呢,於是可能想到逐步替換上麵這些邏輯,比如將最後一個構造函數裏的那些邏輯都禁掉,然後我們再跑測試看看還會不會讓ygc不斷惡化,最終我們會發現,如果我們直接使用如下構造函數構造對象時,如果傳入的classloader是AppClassLoader,那會發現這個問題不再出現了。

 public XStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoader classLoader, Mapper mapper) {
        this(reflectionProvider, driver, new ClassLoaderReference(classLoader), mapper, new DefaultConverterLookup());
 }

測試代碼如下:

 public static void main(String[] args) throws Exception {
        int i=0;
        while (true) {
            XStream xs = new XStream(null,null, new ClassLoaderReference(XStreamTest.class.getClassLoader()),null, new DefaultConverterLookup());
            xs.toString();
            xs=null;
        }
  }

gc日誌如下:

2016-03-14T23:10:33.537+0800: [GC [ParNew: 327680K->758K(368640K), 0.0019803 secs] 327680K->758K(1007616K), 0.0020182 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2016-03-14T23:10:35.189+0800: [GC [ParNew: 328438K->1066K(368640K), 0.0018641 secs] 328438K->1066K(1007616K), 0.0019055 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2016-03-14T23:10:36.465+0800: [GC [ParNew: 328746K->1156K(368640K), 0.0010304 secs] 328746K->1156K(1007616K), 0.0010519 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2016-03-14T23:10:37.767+0800: [GC [ParNew: 328836K->1065K(368640K), 0.0011329 secs] 328836K->1065K(1007616K), 0.0011543 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2016-03-14T23:10:39.035+0800: [GC [ParNew: 328745K->351K(368640K), 0.0043387 secs] 328745K->1127K(1007616K), 0.0043700 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2016-03-14T23:10:40.324+0800: [GC [ParNew: 328031K->160K(368640K), 0.0011579 secs] 328807K->936K(1007616K), 0.0011793 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2016-03-14T23:10:41.610+0800: [GC [ParNew: 327840K->31K(368640K), 0.0007010 secs] 328616K->826K(1007616K), 0.0007219 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2016-03-14T23:10:42.919+0800: [GC [ParNew: 327711K->24K(368640K), 0.0011246 secs] 328506K->819K(1007616K), 0.0011450 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
2016-03-14T23:10:44.196+0800: [GC [ParNew: 327704K->24K(368640K), 0.0006797 secs] 328499K->819K(1007616K), 0.0007586 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

是不是覺得很神奇,由此可見,這個classloader至關重要。

不得不說的類加載器

這裏著重要說的兩個概念是初始類加載器定義類加載器。舉個栗子說吧,AClassLoader->BClassLoader->CClassLoader,表示AClassLoader在加載類的時候會委托BClassLoader類加載器來加載,BClassLoader加載類的時候會委托CClassLoader來加載,假如我們使用AClassLoader來加載X這個類,而X這個類最終是被CClassLoader來加載的,那麼我們稱CClassLoader為X類的定義類加載器,而AClassLoader和BClassLoader分別為X類的初始類加載器,JVM在加載某個類的時候對這三種類加載器都會記錄,記錄的數據結構是一個叫做SystemDictionary的hashtable,其key是根據ClassLoader對象和類名算出來的hash值,而value是真正的由定義類加載器加載的Klass對象,因為初始類加載器和定義類加載器是不同的classloader,因此算出來的hash值也是不同的,因此在SystemDictionary裏會有多項值的value都是指向同一個Klass對象。

那麼JVM為什麼要分這兩種類加載器呢,其實主要是為了快速找到已經加載的類,比如我們已經通過AClassLoader來觸發了對X類的加載,當我們再次使用AClassLoader這個類加載器來加載X這個類的時候就不需要再委托給BClassLoader去找了,因為加載過的類在JVM裏有這個類加載器的直接加載的記錄,隻需要直接返回對應的Klass對象即可。

Demo中的類加載器是否會加載類

我們的demo裏發現構建了一個CompositeClassLoader的類加載器,那到底有沒有用這個類加載器加載類呢,我們可以設置一個斷點在CompositeClassLoader的loadClass方法上,於是看到下麵的堆棧:

main@1, prio=5, in group 'main', status: 'RUNNING'
      at com.thoughtworks.xstream.core.util.CompositeClassLoader.loadClass(CompositeClassLoader.java:53)
      at java.lang.Class.forName0(Class.java:-1)
      at java.lang.Class.forName(Class.java:249)
      at com.thoughtworks.xstream.XStream.buildMapperDynamically(XStream.java:191)
      at com.thoughtworks.xstream.XStream.buildMapper(XStream.java:170)
      at com.thoughtworks.xstream.XStream.<init>(XStream.java:142)
      at com.thoughtworks.xstream.XStream.<init>(XStream.java:116)
      at com.BBBB.main(BBBB.java:15)

可見確實有類加載的動作,根據類加載委托機製,在這個demo中我們能肯定類是交給AppClassLoader來加載的,這樣一來CompositeClassLoader就變成了初始類加載器,而AppClassLoader會是定義類加載器,都會在SystemDictionary裏存在,因此當我們不斷new XStream的時候會不斷new CompositeClassLoader對象,加載類的時候會不斷往SystemDictionary裏插入記錄,從而使SystemDictionary越來越膨脹,那自然而然會想到如果GC過程不斷去掃描這個SystemDictionary的話,那隨著SystemDictionary不斷膨脹,那麼GC的效率也就越低,抱著驗證下猜想的方式我們可以使用perf工具來看看,如果發現cpu占比排前的函數如果都是操作SystemDictionary的,那就基本驗證了我們的說法,下麵是perf工具的截圖,基本證實了這一點。

【image】

SystemDictionary為什麼會影響GC過程

想象一下這麼個情況,我們加載了一個類,然後構建了一個對象(這個對象在eden裏構建)當一個屬性設置到這個類裏,如果gc發生的時候,這個對象是不是要被找出來標活才行,那麼自然而然我們加載的類肯定是我們一項重要的gc root,這樣SystemDictionary就成為了gc過程中的被掃描對象了,事實也是如此,可以看vm的具體代碼:

void SharedHeap::process_strong_roots(bool activate_scope,
                                      bool collecting_perm_gen,
                                      ScanningOption so,
                                      OopClosure* roots,
                                      CodeBlobClosure* code_roots,
                                      OopsInGenClosure* perm_blk) {
  StrongRootsScope srs(this, activate_scope);
  // General strong roots.
  assert(_strong_roots_parity != 0, "must have called prologue code");
  // _n_termination for _process_strong_tasks should be set up stream
  // in a method not running in a GC worker.  Otherwise the GC worker
  // could be trying to change the termination condition while the task
  // is executing in another GC worker.
  if (!_process_strong_tasks->is_task_claimed(SH_PS_Universe_oops_do)) {
    Universe::oops_do(roots);
    // Consider perm-gen discovered lists to be strong.
    //將perm gen的非強引用標記為root的一部分
    perm_gen()->ref_processor()->weak_oops_do(roots);
  }
  // Global (strong) JNI handles
  if (!_process_strong_tasks->is_task_claimed(SH_PS_JNIHandles_oops_do))
    JNIHandles::oops_do(roots);
  // All threads execute this; the individual threads are task groups.
  if (ParallelGCThreads > 0) {
    Threads::possibly_parallel_oops_do(roots, code_roots);
  } else {
    Threads::oops_do(roots, code_roots);
  }
  if (!_process_strong_tasks-> is_task_claimed(SH_PS_ObjectSynchronizer_oops_do))
    ObjectSynchronizer::oops_do(roots);
  if (!_process_strong_tasks->is_task_claimed(SH_PS_FlatProfiler_oops_do))
    FlatProfiler::oops_do(roots);
  if (!_process_strong_tasks->is_task_claimed(SH_PS_Management_oops_do))
    Management::oops_do(roots);
  if (!_process_strong_tasks->is_task_claimed(SH_PS_jvmti_oops_do))
    JvmtiExport::oops_do(roots);

  if (!_process_strong_tasks->is_task_claimed(SH_PS_SystemDictionary_oops_do)) {
    if (so & SO_AllClasses) {
      SystemDictionary::oops_do(roots);
    } else if (so & SO_SystemClasses) {
      SystemDictionary::always_strong_oops_do(roots);
    }
  }

  if (!_process_strong_tasks->is_task_claimed(SH_PS_StringTable_oops_do)) {
    //JavaObjectsInPerm為false,那麼String intern的對象已經class對象都是存在heap裏的,否則都存在perm裏  
    if (so & SO_Strings || (!collecting_perm_gen && !JavaObjectsInPerm)) {
      //雖然不回收perm,但是interned的String對象不在perm裏,那麼還是需要遍曆下StringTable裏的String對象,因為這些對象在heap裏
      StringTable::oops_do(roots);
    }
    if (JavaObjectsInPerm) {
      // Verify the string table contents are in the perm gen
      NOT_PRODUCT(StringTable::oops_do(&assert_is_perm_closure));
    }
  }

  if (!_process_strong_tasks->is_task_claimed(SH_PS_CodeCache_oops_do)) {
    if (so & SO_CodeCache) {
      // (Currently, CMSCollector uses this to do intermediate-strength collections.)
      assert(collecting_perm_gen, "scanning all of code cache");
      assert(code_roots != NULL, "must supply closure for code cache");
      if (code_roots != NULL) {
        CodeCache::blobs_do(code_roots);
      }
    } else if (so & (SO_SystemClasses|SO_AllClasses)) {
      if (!collecting_perm_gen) {
        // If we are collecting from class statics, but we are not going to
        // visit all of the CodeCache, collect from the non-perm roots if any.
        // This makes the code cache function temporarily as a source of strong
        // roots for oops, until the next major collection.
        //
        // If collecting_perm_gen is true, we require that this phase will call
        // CodeCache::do_unloading.  This will kill off nmethods with expired
        // weak references, such as stale invokedynamic targets.
        CodeCache::scavenge_root_nmethods_do(code_roots);
      }
    }
    // Verify that the code cache contents are not subject to
    // movement by a scavenging collection.
    DEBUG_ONLY(CodeBlobToOopClosure assert_code_is_non_scavengable(&assert_is_non_scavengable_closure, /*do_marking=*/ false));
    DEBUG_ONLY(CodeCache::asserted_non_scavengable_nmethods_do(&assert_code_is_non_scavengable));
  }

  if (!collecting_perm_gen) {
    //如果是不回收perm,那找出所有perm指向new的對象  
    // All threads perform this; coordination is handled internally.
    rem_set()->younger_refs_iterate(perm_gen(), perm_blk);//perm的level是-1
  }
  _process_strong_tasks->all_tasks_completed();
}

看上麵的SH_PS_SystemDictionary_oops_do task就知道了,這個就是對SystemDictionary進行掃描。

但是這裏要說的是雖然有對SystemDictionary進行掃描,但是ygc的過程並不會對SystemDictionary進行處理,如果要對它進行處理需要開啟類卸載的vm參數,CMS算法下,CMS GC和Full GC在開啟CMSClassUnloadingEnabled的情況下是可能對類做卸載動作的,此時會對SystemDictionary進行清理,所以當我們在跑上麵demo的時候,通過jmap -dump:live,format=b,file=heap.bin <pid>命令執行完之後,ygc的時間瞬間降下來了,不過又會慢慢回去,這是因為jmap的這個命令會做一次gc,這個gc過程會對SystemDictionary進行清理。

修改VM代碼驗證

很遺憾hotspot目前沒有對ygc的每個task做一個時間的統計,因此無法直接知道是不是SH_PS_SystemDictionary_oops_do這個task導致了ygc的時間變長,為了證明這個結論,我特地修改了一下代碼,在上麵的代碼上加了一行:

if (!_process_strong_tasks->is_task_claimed(SH_PS_SystemDictionary_oops_do)) {
    GCTraceTime t("SystemDictionary_OOPS_DO",PrintGCDetails,true,NULL);
    if (so & SO_AllClasses) {
      SystemDictionary::oops_do(roots);
    } else if (so & SO_SystemClasses) {
      SystemDictionary::always_strong_oops_do(roots);
    }
  }

然後重新編譯,跑我們的demo,測試結果如下:

2016-03-14T23:57:24.293+0800: [GC2016-03-14T23:57:24.294+0800: [ParNew2016-03-14T23:57:24.296+0800: [SystemDictionary_OOPS_DO, 0.0578430 secs]
: 81920K->3184K(92160K), 0.0889740 secs] 81920K->3184K(514048K), 0.0900970 secs] [Times: user=0.27 sys=0.00, real=0.09 secs]
2016-03-14T23:57:28.467+0800: [GC2016-03-14T23:57:28.468+0800: [ParNew2016-03-14T23:57:28.468+0800: [SystemDictionary_OOPS_DO, 0.0779210 secs]
: 85104K->5175K(92160K), 0.1071520 secs] 85104K->5175K(514048K), 0.1080490 secs] [Times: user=0.65 sys=0.00, real=0.11 secs]
2016-03-14T23:57:32.984+0800: [GC2016-03-14T23:57:32.984+0800: [ParNew2016-03-14T23:57:32.984+0800: [SystemDictionary_OOPS_DO, 0.1075680 secs]
: 87095K->8188K(92160K), 0.1434270 secs] 87095K->8188K(514048K), 0.1439870 secs] [Times: user=0.90 sys=0.01, real=0.14 secs]
2016-03-14T23:57:37.900+0800: [GC2016-03-14T23:57:37.900+0800: [ParNew2016-03-14T23:57:37.901+0800: [SystemDictionary_OOPS_DO, 0.1745390 secs]
: 90108K->7093K(92160K), 0.2876260 secs] 90108K->9992K(514048K), 0.2884150 secs] [Times: user=1.44 sys=0.02, real=0.29 secs]

我們會發現YGC的時間變長的時候,SystemDictionary_OOPS_DO的時間也會相應變長多少,因此驗證了我們的說法。

最後更新:2017-04-11 19:32:01

  上一篇:go 如何定位消耗CPU最多的線程
  下一篇:go JDK8在泛型類型推導上的變化