jvm原理之SystemGC源碼分析

概述

JVM的GC一般情況下是JVM本身根據一定的條件觸發的,不過我們還是可以做一些人為的觸發,比如通過jvmti做強制GC,通過System.gc觸發,還可以通過jmap來觸發等,針對每個場景其實我們都可以寫篇文章來做一個介紹,本文重點介紹下System.gc的原理

或許大傢已經知道如下相關的知識

  • system.gc其實是做一次full gc
  • system.gc會暫停整個進程
  • system.gc一般情況下我們要禁掉,使用-XX:+DisableExplicitGC
  • system.gc在cms gc下我們通過-XX:+ExplicitGCInvokesConcurrent來做一次稍微高效點的GC(效果比Full GC要好些)
  • system.gc最常見的場景是RMI/NIO下的堆外內存分配等

如果你已經知道上面這些瞭其實也說明你對System.gc有過一定的瞭解,至少踩過一些坑,但是你是否更深層次地瞭解過它,比如

  • 為什麼CMS GC下-XX:+ExplicitGCInvokesConcurrent這個參數加瞭之後會比真正的Full GC好?
  • 它如何做到暫停整個進程?
  • 堆外內存分配為什麼有時候要配合System.gc?

如果你上面這些疑惑也都知道,那說明你很懂System.gc瞭,那麼接下來的文字你可以不用看啦

JDK裡的System.gc的實現

先貼段代碼吧(java.lang.System)

/**
 * Runs the garbage collector.
 * <p>
 * Calling the <code>gc</code> method suggests that the Java Virtual
 * Machine expend effort toward recycling unused objects in order to
 * make the memory they currently occupy available for quick reuse.
 * When control returns from the method call, the Java Virtual
 * Machine has made a best effort to reclaim space from all discarded
 * objects.
 * <p>
 * The call <code>System.gc()</code> is effectively equivalent to the
 * call:
 * <blockquote><pre>
 * Runtime.getRuntime().gc()
 * </pre></blockquote>
 *
 * @see     java.lang.Runtime#gc()
 */
public static void gc() {
    Runtime.getRuntime().gc();
}

發現主要調用的是Runtime裡的gc方法(java.lang.Runtime)

/**
 * Runs the garbage collector.
 * Calling this method suggests that the Java virtual machine expend
 * effort toward recycling unused objects in order to make the memory
 * they currently occupy available for quick reuse. When control
 * returns from the method call, the virtual machine has made
 * its best effort to recycle all discarded objects.
 * <p>
 * The name <code>gc</code> stands for "garbage
 * collector". The virtual machine performs this recycling
 * process automatically as needed, in a separate thread, even if the
 * <code>gc</code> method is not invoked explicitly.
 * <p>
 * The method {@link System#gc()} is the conventional and convenient
 * means of invoking this method.
 */
public native void gc();

這裡看到gc方法是native的,在java層面隻能到此結束瞭,代碼隻有這麼多,要瞭解更多,可以看方法上面的註釋,不過我們需要更深層次地來瞭解其實現,那還是準備好進入到jvm裡去看看

Hotspot裡System.gc的實現

如何找到native裡的實現

上面提到瞭Runtime.gc是一個本地方法,那需要先在jvm裡找到對應的實現,這裡稍微提一下jvm裡native方法最常見的也是最簡單的查找,jdk裡一般含有native方法的類,一般都會有一個對應的c文件,比如上面的java.lang.Runtime這個類,會有一個Runtime.c的文件和它對應,native方法的具體實現都在裡面瞭,如果你有source,可能會猜到和下面的方法對應

JNIEXPORT void JNICALL
Java_java_lang_Runtime_gc(JNIEnv *env, jobject this)
{
    JVM_GC();
}

其實沒錯的,就是這個方法,jvm要查找到這個native方法其實很簡單的,看方法名可能也猜到規則瞭,Java_pkgName_className_methodName,其中pkgName裡的".“替換成”_“,這樣就能找到瞭,當然規則不僅僅隻有這麼一個,還有其他的,這裡不細說瞭,有機會寫篇文章詳細介紹下其中細節

DisableExplicitGC參數

上面的方法裡是調用JVM_GC(),實現如下

JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  JVMWrapper("JVM_GC");
  if (!DisableExplicitGC) {
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
  }
JVM_END

看到這裡我們已經解釋其中一個疑惑瞭,就是DisableExplicitGC這個參數是在哪裡生效的,起的什麼作用,如果這個參數設置為true的話,那麼將直接跳過下面的邏輯,我們通過-XX:+ DisableExplicitGC就是將這個屬性設置為true,而這個屬性默認情況下是true還是false呢

product(bool, DisableExplicitGC, false,                                   \
          "Tells whether calling System.gc() does a full GC")    

ExplicitGCInvokesConcurrent參數

這裡主要針對CMSGC下來做分析,所以我們上面看到調用瞭heap的collect方法,我們找到對應的邏輯

void GenCollectedHeap::collect(GCCause::Cause cause) {
  if (should_do_concurrent_full_gc(cause)) {
#ifndef SERIALGC
    // mostly concurrent full collection
    collect_mostly_concurrent(cause);
#else  // SERIALGC
    ShouldNotReachHere();
#endif // SERIALGC
  } else {
#ifdef ASSERT
    if (cause == GCCause::_scavenge_alot) {
      // minor collection only
      collect(cause, 0);
    } else {
      // Stop-the-world full collection
      collect(cause, n_gens() - 1);
    }
#else
    // Stop-the-world full collection
    collect(cause, n_gens() - 1);
#endif
  }
}

bool GenCollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) {
  return UseConcMarkSweepGC &&
         ((cause == GCCause::_gc_locker && GCLockerInvokesConcurrent) ||
          (cause == GCCause::_java_lang_system_gc && ExplicitGCInvokesConcurrent));
}

collect裡一開頭就有個判斷,如果should_do_concurrent_full_gc返回true,那會執行collect_mostly_concurrent做並行的回收

其中should_do_concurrent_full_gc中的邏輯是如果使用CMS GC,並且是system gc且ExplicitGCInvokesConcurrent==true,那就做並行full gc,當我們設置-XX:+ ExplicitGCInvokesConcurrent的時候,就意味著應該做並行Full GC瞭,不過要註意千萬不要設置-XX:+DisableExplicitGC,不然走不到這個邏輯裡來瞭

並行Full GC相對正常的Full GC效率高在哪裡

stop the world

說到GC,這裡要先提到VMThread,在jvm裡有這麼一個線程不斷輪詢它的隊列,這個隊列裡主要是存一些VM_operation的動作,比如最常見的就是內存分配失敗要求做GC操作的請求等,在對gc這些操作執行的時候會先將其他業務線程都進入到安全點,也就是這些線程從此不再執行任何字節碼指令,隻有當出瞭安全點的時候才讓他們繼續執行原來的指令,因此這其實就是我們說的stop the world(STW),整個進程相當於靜止瞭

CMS GC

這裡必須提到CMS GC,因為這是解釋並行Full GC和正常Full GC的關鍵所在,CMS GC我們分為兩種模式background和foreground,其中background顧名思義是在後臺做的,也就是可以不影響正常的業務線程跑,觸發條件比如說old的內存占比超過多少的時候就可能觸發一次background式的cms gc,這個過程會經歷CMS GC的所有階段,該暫停的暫停,該並行的並行,效率相對來說還比較高,畢竟有和業務線程並行的gc階段;而foreground則不然,它發生的場景比如業務線程請求分配內存,但是內存不夠瞭,於是可能觸發一次cms gc,這個過程就必須是要等內存分配到瞭線程才能繼續往下面走的,因此整個過程必須是STW的,因此CMS GC整個過程都是暫停應用的,但是為瞭提高效率,它並不是每個階段都會走的,隻走其中一些階段,這些省下來的階段主要是並行階段,Precleaning、AbortablePreclean,Resizing這幾個階段都不會經歷,其中sweep階段是同步的,但不管怎麼說如果走瞭類似foreground的cms gc,那麼整個過程業務線程都是不可用的,效率會影響挺大。CMS GC具體的過程後面再寫文章詳細說,其過程確實非常復雜的

正常的Full GC

正常的Full GC其實是整個gc過程包括ygc和cms gc(這裡說的是真正意義上的Full GC,還有些場景雖然調用Full GC的接口,但是並不會都做,有些時候隻做ygc,有些時候隻做cms gc)都是由VMThread來執行的,因此整個時間是ygc+cms gc的時間之和,其中CMS GC是上面提到的foreground式的,因此整個過程會比較長,也是我們要避免的

並行的Full GC

並行Full GC也通樣會做YGC和CMS GC,但是效率高就搞在CMS GC是走的background的,整個暫停的過程主要是YGC+CMS_initMark+CMS_remark幾個階段

堆外內存常配合使用System GC

這裡說的堆外內存主要針對java.nio.DirectByteBuffer,這些對象的創建過程會通過Unsafe接口直接通過os::malloc來分配內存,然後將內存的起始地址和大小存到java.nio.DirectByteBuffer對象裡,這樣就可以直接操作這些內存。這些內存隻有在DirectByteBuffer回收掉之後才有機會被回收,因此如果這些對象大部分都移到瞭old,但是一直沒有觸發CMS GC或者Full GC,那麼悲劇將會發生,因為你的物理內存被他們耗盡瞭,因此為瞭避免這種悲劇的發生,通過-XX:MaxDirectMemorySize來指定最大的堆外內存大小,當使用達到瞭閾值的時候將調用System.gc來做一次full gc,以此來回收掉沒有被使用的堆外內存。

具體堆外內存是如何回收的,其原理機制又是怎樣的,後面文章會詳細寫出,請大傢持續關註WalkonNet~

推薦閱讀: