System.gc 之後到底發生了什麼 ?

bin的技术小屋發表於2024-04-01

本文基於 OpenJDK17 進行討論

在 JDK NIO 針對堆外記憶體的分配場景中,我們經常會看到 System.gc 的身影,比如當我們透過 FileChannel#map 對檔案進行記憶體對映的時候,如果 JVM 程序虛擬記憶體空間中的虛擬記憶體不足,JVM 在 native 層就會丟擲 OutOfMemoryError

當 JDK 捕獲到 OutOfMemoryError 異常的時候,就會意識到此時程序虛擬記憶體空間中的虛擬記憶體已經不足了,無法支援本次記憶體對映,於是就會呼叫 System.gc 強制觸發一次 GC ,試圖釋放一些虛擬記憶體出來,然後再次嘗試來 mmap 一把,如果程序地址空間中的虛擬記憶體還是不足,則丟擲 IOException

private Unmapper mapInternal(MapMode mode, long position, long size, int prot, boolean isSync)
        throws IOException
{
            try {
                    // If map0 did not throw an exception, the address is valid
                    addr = map0(prot, mapPosition, mapSize, isSync);
                } catch (OutOfMemoryError x) {
                    // An OutOfMemoryError may indicate that we've exhausted
                    // memory so force gc and re-attempt map
                    System.gc();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException y) {
                        Thread.currentThread().interrupt();
                    }
                    try {
                        addr = map0(prot, mapPosition, mapSize, isSync);
                    } catch (OutOfMemoryError y) {
                        // After a second OOME, fail
                        throw new IOException("Map failed", y);
                    }
              }

}

再比如,我們透過 ByteBuffer#allocateDirect 申請堆外記憶體的時候

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
   public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
}

會首先透過 Bits.reserveMemory 檢查當前 JVM 程序的堆外記憶體用量是否超過了 -XX:MaxDirectMemorySize 指定的最大堆外記憶體限制,透過檢查之後才會呼叫 UNSAFE.allocateMemory 申請堆外記憶體。

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
    DirectByteBuffer(int cap) {                   // package-private

        ...... 省略 .....   
        // 檢查堆外記憶體整體用量是否超過了 -XX:MaxDirectMemorySize
        // 如果超過則嘗試對堆外記憶體進行回收,回收之後還不夠的話則丟擲 OutOfMemoryError
        Bits.reserveMemory(size, cap);   
        // 底層呼叫 malloc 申請虛擬記憶體
        base = UNSAFE.allocateMemory(size);

        ...... 省略 .....   
    }
}

如果沒有透過 Bits.reserveMemory 的檢查,則 JVM 先會嘗試透過釋放當前已經被回收的 direct buffer 背後引用的 native memory 挽救一下,如果釋放之後堆外記憶體容量還是不夠,那麼就觸發 System.gc()。

class Bits {  
    static void reserveMemory(long size, long cap) {
        ...... 省略 .......
        // 如果本次申請的堆外內容容量 cap 已經超過了 -XX:MaxDirectMemorySize
        // 則返回 false,表示無法滿足本次堆外記憶體的申請
        if (tryReserveMemory(size, cap)) {
            return;
        }

        ...... 嘗試釋放已經被回收的 directBuffer 背後的 native memory  .......
        // 在已經被回收的 direct buffer 背後引用的 native memory 被釋放之後
        // 如果還是不夠,則走到這裡
        System.gc();
        Thread.sleep(sleepTime);

        ...... 省略 .......
    }
}

通常情況下我們應當避免在應用程式中主動呼叫 System.gc,因為這會導致 JVM 立即觸發一次 Full GC,使得整個 JVM 程序陷入到 Stop The World 階段,對效能會有很大的影響。

但是在 NIO 的場景中,呼叫 System.gc 卻是有必要的,因為 NIO 中的 DirectByteBuffer 非常特殊,當然了 MappedByteBuffer 其實也屬於 DirectByteBuffer 的一種。它們背後依賴的記憶體均屬於 JVM 之外(Native Memory),因此不會受垃圾回收的控制。

前面我們多次提過,DirectByteBuffer 只是 OS 中的這些 Native Memory 在 JVM 中的封裝形式,DirectByteBuffer 這個 Java 類的例項是分配在 JVM 堆中的,但是這個例項的背後可能會引用著一大片的 Native Memory ,這些 Native Memory 是不會被 JVM 察覺的。

當這些 DirectByteBuffer 例項(位於 JVM 堆中)沒有任何引用的時候,如果又恰巧碰到 GC 的話,那麼 GC 在回收這些 DirectByteBuffer 例項的同時,也會將與其關聯的 Cleaner 放到一個 pending 佇列中。

    protected DirectByteBuffer(int cap, long addr,
                                     FileDescriptor fd,
                                     Runnable unmapper,
                                     boolean isSync, MemorySegmentProxy segment)
    {
        super(-1, 0, cap, cap, fd, isSync, segment);
        address = addr;
        // 對於 MappedByteBuffer 來說,在它被 GC 的時候,JVM 會呼叫這裡的 cleaner
        // cleaner 近而會呼叫 Unmapper#unmap 釋放背後的 native memory
        cleaner = Cleaner.create(this, unmapper);
        att = null;
    }

當 GC 結束之後,JVM 會喚醒 ReferenceHandler 執行緒去執行 pending 佇列中的這些 Cleaner,在 Cleaner 中會釋放其背後引用的 Native Memory。

但在現實的 NIO 使用場景中,DirectByteBuffer 卻很難觸發 GC,因為 DirectByteBuffer 的例項實在太小了(在 JVM 堆中的記憶體佔用),而且通常情況下這些例項是被應用程式長期持有的,很容易就會晉升到老年代。

即使 DirectByteBuffer 例項已經沒有任何引用關係了,由於它的例項足夠的小,一時很難把老年代撐爆,所以需要等很久才能觸發一次 Full GC,在這之前,這些沒有任何引用關係的 DirectByteBuffer 例項將會持續在老年代中堆積,其背後所引用的大片 Native Memory 將一直不會得到釋放。

DirectByteBuffer 的例項可以形象的比喻為冰山物件,JVM 可以看到的只是 DirectByteBuffer 在 JVM 堆中的記憶體佔用,但這部分記憶體佔用很小,就相當於是冰山的一角。

image

而位於冰山下面的大一片 Native Memory ,JVM 是察覺不到的, 這也是 Full GC 遲遲不會觸發的原因,因此導致了大量的 DirectByteBuffer 例項的堆積,背後引用的一大片 Native Memory 一直得不到釋放,嚴重的情況下可能會導致核心的 OOM,當前程序會被 kill 。

所以在 NIO 的場景下,這裡呼叫 System.gc 去主動觸發一次 Full GC 是有必要的。關於 System.gc ,網上的說法眾多,其中大部分認為 —— “System.gc 只是給 JVM 的一個暗示或者是提示,但是具體 GC 會不會發生,以及什麼時候發生都是不可預期的”。

這個說法以及 Java 標準庫中關於 System.gc 的註釋都是非常模糊的,那麼在 System.gc 被呼叫之後具體會發生什麼行為,我想還是應該到具體的 JVM 實現中去一探究竟,畢竟原始碼面前了無秘密,下面我們以 hotspot 實現進行說明。

public final class System {
   public static void gc() {
        Runtime.getRuntime().gc();
    }
}

public class Runtime {
   public native void gc();
}

System.gc 最終依賴的是 Runtime 類中定義的 gc 方法,該方法是一個 native 實現,定義在 Runtime.c 檔案中。

// Runtime.c 檔案
JNIEXPORT void JNICALL
Java_java_lang_Runtime_gc(JNIEnv *env, jobject this)
{
    JVM_GC();
}
// jvm.cpp 檔案
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  // DisableExplicitGC 預設為 false,如果設定了 -XX:+DisableExplicitGC 則為 true
  if (!DisableExplicitGC) {
    EventSystemGC event;
    event.set_invokedConcurrent(ExplicitGCInvokesConcurrent);
    // 立即觸發一次  full gc
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
    event.commit();
  }
JVM_END

從 hotspot 的實現中我們可以看出,如果我們設定了 -XX:+DisableExplicitGC,那麼呼叫 System.gc 則不會起任何作用,在預設情況下,System.gc 會立即觸發一次 Full GC,這一點我們可以從 Universe::heap()->collect 方法的呼叫看得出來。而且會特殊註明引起本次 GC 的原因 GCCause 為 _java_lang_system_gc

JVM 堆的例項封裝在 Universe 類中,我們可以透過 heap() 方法來獲取 JVM 堆的例項,隨後呼叫堆的 collect 方法在 JVM 堆中執行垃圾回收的動作。

// universe.hpp 檔案
// jvm 堆例項
static CollectedHeap* _collectedHeap;
static CollectedHeap* heap() { return _collectedHeap; }

Java 堆在 JVM 原始碼中使用 CollectedHeap 型別來描述,該型別為整個 JVM 堆結構型別的基類,具體的實現型別取決於我們選擇的垃圾回收器。比如,當我們選擇 ZGC 作為垃圾回收器時,JVM 堆的型別是 ZCollectedHeap,選擇 G1 作為垃圾回收器時,JVM 堆的型別則是 G1CollectedHeap。

JVM 在初始化堆的時候,會透過 GCConfig::arguments()->create_heap() 根據我們選擇的具體垃圾回收器來建立相應的堆型別,具體的 JVM 堆例項會儲存在 _collectedHeap 中,後續透過 Universe::heap() 即可獲取。

// universe.cpp 檔案
// jvm 堆例項
CollectedHeap*  Universe::_collectedHeap = NULL;

jint Universe::initialize_heap() {
  assert(_collectedHeap == NULL, "Heap already created");
  // 根據 JVM 引數  -XX: 指定的相關 gc 配置建立對應的 heap
  // 比如,設定了 -XX:+UseZGC,這裡建立的就是 ZCollectedHeap
  _collectedHeap = GCConfig::arguments()->create_heap();

  log_info(gc)("Using %s", _collectedHeap->name());
  // 初始化 jvm 堆
  return _collectedHeap->initialize();
}

GCConfig 是 JVM 專門用於封裝 GC 相關資訊的類,具體建立堆的行為 —— create_heap(),則封裝在 GCConfig 類中的 _arguments 屬性中(GCArguments 型別)。這裡也是一樣,不同的垃圾回收器對應不同的 GCArguments,比如,ZGC 對應的是 ZArguments,G1 對應的是 G1Arguments。典型工廠,策略模式的應用,不同的 GCArguments 負責建立不用型別的 JVM 堆。

// gcConfig.cpp 檔案
GCArguments* GCConfig::arguments() {
  assert(_arguments != NULL, "Not initialized");
  // 真正負責建立 jvm 堆的類
  return _arguments;
}

JVM 在啟動的時候會對 GCConfig 進行初始化,透過 select_gc() 根據我們指定的 -XX: 相關 GC 配置選項來選擇具體的 _arguments,比如,我們設定了 -XX:+UseZGC, 這裡的 select_gc 就會返回 ZArguments 例項,並儲存在 _arguments 屬性中,隨後我們就可以透過 GCConfig::arguments() 獲取。

void GCConfig::initialize() {
  assert(_arguments == NULL, "Already initialized");
  _arguments = select_gc();
}

select_gc() 的邏輯其實非常簡單,核心就是遍歷一個叫做 IncludedGCs 的陣列,該陣列裡包含的是當前 JVM 版本中所支援的所有垃圾回收器集合。比如,當我們透過 command line 指定了 -XX:+UseZGC 的時候,相關的 GC 引數 UseZGC 就會為 true,其他的 GC 引數都為 false,如果 JVM 在遍歷 IncludedGCs 陣列的時候發現,當前遍歷元素的 GC 引數為 true,那麼就會將對應的 _arguments (zArguments)返回。

// gcConfig.cpp 檔案
// Table of included GCs, for translating between command
// line flag, CollectedHeap::Name and GCArguments instance.
static const IncludedGC IncludedGCs[] = {
   EPSILONGC_ONLY_ARG(IncludedGC(UseEpsilonGC,       CollectedHeap::Epsilon,    epsilonArguments,    "epsilon gc"))
        G1GC_ONLY_ARG(IncludedGC(UseG1GC,            CollectedHeap::G1,         g1Arguments,         "g1 gc"))
  PARALLELGC_ONLY_ARG(IncludedGC(UseParallelGC,      CollectedHeap::Parallel,   parallelArguments,   "parallel gc"))
    SERIALGC_ONLY_ARG(IncludedGC(UseSerialGC,        CollectedHeap::Serial,     serialArguments,     "serial gc"))
SHENANDOAHGC_ONLY_ARG(IncludedGC(UseShenandoahGC,    CollectedHeap::Shenandoah, shenandoahArguments, "shenandoah gc"))
         ZGC_ONLY_ARG(IncludedGC(UseZGC,             CollectedHeap::Z,          zArguments,          "z gc"))
};

IncludedGCs 陣列的元素型別為 IncludedGC,用於封裝具體垃圾回收器的相關配置資訊:

// gcConfig.cpp 檔案
struct IncludedGC {
  // GCArgument,如果我們透過 command line 配置了具體的垃圾回收器
  // 那麼對應的 IncludedGC 型別中的 _flag 就為 true。
  // -XX:+UseG1GC 對應 UseG1GC,-XX:+UseZGC 對應 UseZGC
  bool&               _flag;
  // 具體垃圾回收器的名稱
  CollectedHeap::Name _name;
  // 對應的 GCArguments,後續用於 create_heap
  GCArguments&        _arguments;
  const char*         _hs_err_name;
};

select_gc() 就是遍歷這個 IncludedGCs 陣列,查詢 _flag 為 true 的陣列項,然後返回其 _arguments。

GCArguments* GCConfig::select_gc() {
  // 遍歷 IncludedGCs 陣列
  FOR_EACH_INCLUDED_GC(gc) {
    // GCArgument 為 true 則返回對應的 _arguments
    if (gc->_flag) {
      return &gc->_arguments;
    }
  }
  return NULL;
}

#define FOR_EACH_INCLUDED_GC(var)                                            \
  for (const IncludedGC* var = &IncludedGCs[0]; var < &IncludedGCs[ARRAY_SIZE(IncludedGCs)]; var++)

當我們透過設定 -XX:+UseG1GC 選擇 G1 垃圾回收器的時候,對應在 GCConfig 中的 _arguments 為 G1Arguments ,透過 GCConfig::arguments()->create_heap() 建立出來的 JVM 堆的型別為 G1CollectedHeap。

CollectedHeap* G1Arguments::create_heap() {
  return new G1CollectedHeap();
}

同理,當我們透過設定 -XX:+UseZGC 選擇 ZGC 垃圾回收器的時候,JVM 堆的型別為 ZCollectedHeap。

CollectedHeap* ZArguments::create_heap() {
  return new ZCollectedHeap();
}

當我們透過設定 -XX:+UseSerialGC 選擇 SerialGC 垃圾回收器的時候,JVM 堆的型別為 SerialHeap。

CollectedHeap* SerialArguments::create_heap() {
  return new SerialHeap();
}

當我們透過設定 -XX:+UseParallelGC 選擇 ParallelGC 垃圾回收器的時候,JVM 堆的型別為 ParallelScavengeHeap。

CollectedHeap* ParallelArguments::create_heap() {
  return new ParallelScavengeHeap();
}

當我們透過設定 -XX:+UseShenandoahGC 選擇 Shenandoah 垃圾回收器的時候,JVM 堆的型別為 ShenandoahHeap。

CollectedHeap* ShenandoahArguments::create_heap() {
  return new ShenandoahHeap(new ShenandoahCollectorPolicy());
}

現在我們已經明確了各個垃圾回收器對應的 JVM 堆型別,而 System.gc 本質上呼叫的其實就是具體 JVM 堆中的 collect 方法來立即觸發一次 Full GC。

// jvm.cpp 檔案
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  if (!DisableExplicitGC) {
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
  }
JVM_END

下面我們就來結合具體的垃圾回收器看一下 System.gc 的行為,長話短說,先把結論丟擲來:

  • 如果我們在 command line 中設定了 -XX:+DisableExplicitGC,那麼呼叫 System.gc 則不會起任何作用。

  • 如果我們選擇的垃圾回收器是 SerialGC,ParallelGC,ZGC 的話,那麼呼叫 System.gc 就會立即觸發一次 Full GC,整個 JVM 程序會陷入 Stop The World 階段,呼叫 System.gc 的執行緒會一直阻塞,直到整個 Full GC 結束才會返回。

  • 如果我們選擇的垃圾回收器是 CMS(已在 Java 9 中廢棄),G1,Shenandoah,並且在 command line 中設定了 -XX:+ExplicitGCInvokesConcurrent 的話,那麼在呼叫 System.gc 則會立即觸發一次 Concurrent Full GC,JVM 程序不會陷入 Stop The World 階段,業務執行緒和 GC 執行緒可以併發執行,而且呼叫 System.gc 的執行緒在觸發 Concurrent Full GC 之後就立即返回了,不需要等到 GC 結束。

1. SerialGC

對於 SerialGC 來說,在呼叫 System.gc 之後,JVM 背後其實直接呼叫的是 SerialHeap 的 collect 方法。

// serialHeap.hpp 檔案
class SerialHeap : public GenCollectedHeap {

}

由於 SerialHeap 繼承的是 GenCollectedHeap,collect 方法是在 GenCollectedHeap 中實現的。

// genCollectedHeap.cpp 檔案
void GenCollectedHeap::collect(GCCause::Cause cause) {
    // GCCause 為 _java_lang_system_gc 的時候會呼叫到這裡
    // Stop-the-world full collection.
    collect(cause, OldGen);
}
void GenCollectedHeap::collect(GCCause::Cause cause, GenerationType max_generation) {
  collect_locked(cause, max_generation);
}

void GenCollectedHeap::collect_locked(GCCause::Cause cause, GenerationType max_generation) {
    // 在這裡會觸發 Full Gc 的執行
    VM_GenCollectFull op(gc_count_before, full_gc_count_before,
                         cause, max_generation);
    // 提交給 VMThread 來執行 Full Gc
    VMThread::execute(&op);
}

這裡需要注意的是執行這段程式碼的執行緒依然是呼叫 System.gc 的 Java 業務執行緒,而 JVM 內部的相關操作,比如這裡的 GC 操作,均是由 JVM 中的 VMThread 來執行的。

所以這裡 Java 業務執行緒需要將 Full Gc 的任務 —— VM_GenCollectFull 透過 VMThread::execute(&op) 提交給 VMThread 來執行。而 Java 業務執行緒一直會在這裡阻塞等待,直到 VMThread 執行完 Full Gc 之後,Java 業務執行緒才會從 System.gc 呼叫中返回。

這樣設計也是合理的,因為畢竟 Full Gc 會讓整個 JVM 程序陷入 Stop The World 階段,所有 Java 執行緒必須到達 SafePoint 之後 Full Gc 才會執行,而我們透過 JNI 進入到 Native 方法的實現之後,由於 Native 程式碼不會訪問 Java 物件、不會呼叫 Java 方法,不再執行任何位元組碼指令,所以 Java 虛擬機器的堆疊不會發生改變,因此 Native 方法本身就是一個 SafePoint。在 Full Gc 沒有結束之前,Java 執行緒會一直停留在這個 SafePoint 中。

void VMThread::execute(VM_Operation* op) {
  // 獲取當前執行執行緒
  Thread* t = Thread::current();

  if (t->is_VM_thread()) {
    // 如果當前執行緒是 VMThread 的話,直接執行 VM_Operation(Full Gc)
    ((VMThread*)t)->inner_execute(op);
    return;
  }

  // doit_prologue 為執行 VM_Operation 的前置回撥函式,Full Gc 之前執行一些準備校驗工作。
  // 返回 true 表示可以執行本次 GC 操作, 返回 false 表示忽略本次 GC
  // JVM 可能會觸發多次 GC 請求,比如多個 java 執行緒遇到分配失敗的時候
  // 但我們只需要執行一次 GC 就可以了,其他 GC 請求在這裡就會被忽略
  // 另外執行 GC 之前需要給 JVM 堆加鎖,heap lock 也是在這裡完成的。
  if (!op->doit_prologue()) {
    return;   // op was cancelled
  }
  // java 執行緒將 Full Gc 的任務提交給 VMThread 執行
  // 並且會在這裡一直阻塞等待,直到 Full Gc 執行完畢。
  wait_until_executed(op);
  // 釋放 heap lock,喚醒 ReferenceHandler 執行緒去執行 pending 佇列中的 Cleaner
  op->doit_epilogue();
}

注意這裡的 op->doit_epilogue() 方法,在 GC 結束之後就會呼叫到這裡,而與 DirectByteBuffer 相關聯的 Cleaner 正是在這裡被觸發執行的。

void VM_GC_Operation::doit_epilogue() {

  if (Universe::has_reference_pending_list()) {
    // 通知 cleaner thread 執行 cleaner,release native memory
    Heap_lock->notify_all();
  }
  // Heap_lock->unlock()
  VM_GC_Sync_Operation::doit_epilogue();
}

2. ParallelGC

對於 ParallelGC 來說,在呼叫 System.gc 之後,JVM 背後其實直接呼叫的是 ParallelScavengeHeap 的 collect 方法。

// This method is used by System.gc() and JVMTI.
void ParallelScavengeHeap::collect(GCCause::Cause cause) {
 
  VM_ParallelGCSystemGC op(gc_count, full_gc_count, cause);
  VMThread::execute(&op);
}

我們透過下面的 is_cause_full 方法可以知道 VM_ParallelGCSystemGC 執行的也是 Full Gc,同樣也是需要將 Full Gc 任務提交給 VMThread 執行,Java 業務執行緒在這裡阻塞等待直到 Full Gc 完成。

// Only used for System.gc() calls
VM_ParallelGCSystemGC::VM_ParallelGCSystemGC(uint gc_count,
                                             uint full_gc_count,
                                             GCCause::Cause gc_cause) :
  VM_GC_Operation(gc_count, gc_cause, full_gc_count, is_cause_full(gc_cause))
{
}
// 對於 System.gc  來說這裡執行的是 full_gc
static bool is_cause_full(GCCause::Cause cause) {
  return (cause != GCCause::_gc_locker) && (cause != GCCause::_wb_young_gc)
         DEBUG_ONLY(&& (cause != GCCause::_scavenge_alot));
}

3. ZGC

對於 ZGC 來說,在呼叫 System.gc 之後,JVM 背後其實直接呼叫的是 ZCollectedHeap 的 collect 方法。JVM 會執行一個同步的 GC 操作,Java 業務執行緒仍然會在這裡阻塞,直到 GC 完成才會返回。

// zCollectedHeap.cpp 檔案
void ZCollectedHeap::collect(GCCause::Cause cause) {
  _driver->collect(cause);
}

// zDriver.cpp 檔案
void ZDriver::collect(const ZDriverRequest& request) {
  switch (request.cause()) {
  // System.gc
  case GCCause::_java_lang_system_gc:
    // Start synchronous GC
    _gc_cycle_port.send_sync(request);
    break;

  ..... 省略 ,,,,,,
  }
}
template <typename T>
inline void ZMessagePort<T>::send_sync(const T& message) {
  Request request;

  {
    // Enqueue message
    // 隨後 ZDriver 執行緒會非同步從佇列中取出 message,執行 gc
    MonitorLocker ml(&_monitor, Monitor::_no_safepoint_check_flag);
    request.initialize(message, _seqnum);
    _queue.insert_last(&request);
    // 喚醒 ZDriver 執行緒執行 gc
    ml.notify();
  }

  // java 業務執行緒在這裡阻塞等待,直到 gc 完成
  request.wait();
}

4. G1

對於 G1 來說,在呼叫 System.gc 之後,JVM 背後其實直接呼叫的是 G1CollectedHeap 的 collect 方法。

// g1CollectedHeap.cpp 檔案
void G1CollectedHeap::collect(GCCause::Cause cause) {
  try_collect(cause);
}

G1 這裡首先會透過 should_do_concurrent_full_gc 方法判斷是否發起一次 Concurrent Full GC,從下面的原始碼中可以看出,對於 System.gc 來說,該方法其實是對 ExplicitGCInvokesConcurrent 這個 GC 引數的判斷。

當我們在 command line 中設定了 -XX:+ExplicitGCInvokesConcurrent 的話,ExplicitGCInvokesConcurrent 為 true,預設為 false。

bool G1CollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) {
  switch (cause) {
    case GCCause::_g1_humongous_allocation: return true;
    case GCCause::_g1_periodic_collection:  return G1PeriodicGCInvokesConcurrent;
    case GCCause::_wb_breakpoint:           return true;
    // System.gc 會走這裡的 default 分支
    default:                                return is_user_requested_concurrent_full_gc(cause);
  }
}

bool  G1CollectedHeap::is_user_requested_concurrent_full_gc(GCCause::Cause cause) {
  switch (cause) {
    // System.gc
    case GCCause::_java_lang_system_gc:                 return ExplicitGCInvokesConcurrent;

    ...... 省略 .....
  }
}

當我們設定了 -XX:+ExplicitGCInvokesConcurrent 的時候,System.gc 就會觸發一次 Concurrent Full GC,GC 過程不需要經歷 Stop The World 階段,由 G1 相關的 Concurrent GC 執行緒來執行 Concurrent Full GC 而不是之前的 VMThread。

而且呼叫 System.gc 的 Java 業務執行緒在觸發 Concurrent Full GC 之後就返回了,不需要等到 GC 執行完畢。

但在預設情況下,也就是沒有設定 -XX:+ExplicitGCInvokesConcurrent 的時候,仍然會執行一次完整的 Full GC。

bool G1CollectedHeap::try_collect(GCCause::Cause cause) {
  assert_heap_not_locked();
  // -XX:+ExplicitGCInvokesConcurrent
  if (should_do_concurrent_full_gc(cause)) {
    // 由 Concurrent GC 執行緒來執行
    return try_collect_concurrently(cause,
                                    gc_count_before,
                                    old_marking_started_before);
  }  else {
    // Schedule a Full GC.
    VM_G1CollectFull op(gc_count_before, full_gc_count_before, cause);
    VMThread::execute(&op);
    return op.gc_succeeded();
  }
}

對於 CMS 來說,雖然它已經在 Java 9 中被廢棄了,但從 Java 8 的原始碼中可以看出,CMS 這裡的邏輯(System.gc )和 G1 是一樣的,首先都會透過 should_do_concurrent_full_gc 方法來判斷是否執行一次 Concurrent Full GC,都是取決於是否設定了 -XX:+ExplicitGCInvokesConcurrent ,否則執行完整的 Full GC。

5. Shenandoah

對於 Shenandoah 來說,在呼叫 System.gc 之後,JVM 背後其實直接呼叫的是 ShenandoahHeap 的 collect 方法。

void ShenandoahHeap::collect(GCCause::Cause cause) {
  control_thread()->request_gc(cause);
}

首先會透過 is_user_requested_gc 方法判斷本次 GC 是否是由 System.gc 所觸發的,如果是,則進入 handle_requested_gc 中處理,GCCause 為 java_lang_system_gc 。

// gcCause.hpp 檔案
 inline static bool is_user_requested_gc(GCCause::Cause cause) {
    return (cause == GCCause::_java_lang_system_gc ||
            cause == GCCause::_dcmd_gc_run);
  }

如果我們在 command line 中設定了 -XX:+DisableExplicitGC,那麼這裡的 System.gc 將不會起任何作用。

// shenandoahControlThread.cpp
void ShenandoahControlThread::request_gc(GCCause::Cause cause) {
  assert(GCCause::is_user_requested_gc(cause) || ....... ,"only requested GCs here");
  // System.gc 
  if (is_explicit_gc(cause)) {
    if (!DisableExplicitGC) {
      // 沒有設定 -XX:+DisableExplicitGC 的情況下會走這裡
      handle_requested_gc(cause);
    }
  } else {
    handle_requested_gc(cause);
  }
}

bool ShenandoahControlThread::is_explicit_gc(GCCause::Cause cause) const {
  return GCCause::is_user_requested_gc(cause) ||
         GCCause::is_serviceability_requested_gc(cause);
}

呼叫 System.gc 的 Java 業務執行緒首先在 handle_requested_gc 方法中會設定 gc 請求標誌 _gc_requested.set,ShenandoahControlThread 會定時檢測這個 _gc_requested 標誌,如果被設定了,則進行後續的 GC 處理。

Java 業務執行緒最後會一直阻塞在 handle_requested_gc 方法中,如果進行的是 Concurrent Full GC 的話,那麼 GC 任務在被提交給對應的 Concurrent GC 執行緒之後就會喚醒 Java 業務執行緒。如果執行的是 Full GC 的話,那麼當 VMthread 執行完 Full GC 的時候才會喚醒阻塞在這裡的 Java 業務執行緒,隨後 Java 執行緒從 System.gc 呼叫中返回。

void ShenandoahControlThread::handle_requested_gc(GCCause::Cause cause) {

  MonitorLocker ml(&_gc_waiters_lock);

  while (current_gc_id < required_gc_id) {
    // 設定 gc 請求標誌,後續會由 ShenandoahControlThread 來執行
    _gc_requested.set();
    // java_lang_system_gc 
    _requested_gc_cause = cause;

    if (cause != GCCause::_wb_breakpoint) {
      // java 業務執行緒會在這裡阻塞等待
      // 對於 Concurrent Full GC 來說,GC 在被觸發的時候,java 執行緒就會被喚醒直接返回
      // 對於 Full GC 來說,java 執行緒需要等到 gc 被執行完才會被喚醒
      ml.wait();
    }
  }
}

ShenandoahControlThread 會根據一定的間隔時間來檢測 _gc_requested 標誌是否被設定,如果被設定則繼續後續的 GC 處理:

  • 如果我們設定了 -XX:+ExplicitGCInvokesConcurrent,Shenandoah 會觸發一次 Concurrent Full GC ,否則進行的是 Full GC ,這一點和 G1 的處理方式是一樣的。

  • 最後透過 notify_gc_waiters() 喚醒在 handle_requested_gc 中阻塞等待的 java 執行緒。

void ShenandoahControlThread::run_service() {
  ShenandoahHeap* heap = ShenandoahHeap::heap();
  // 預設的一些設定,後面會根據配置修改
  GCMode default_mode = concurrent_normal;// 併發模式
  GCCause::Cause default_cause = GCCause::_shenandoah_concurrent_gc;

  while (!in_graceful_shutdown() && !should_terminate()) {
        // _gc_requested 如果被設定,後續則會處理  System.gc  的邏輯
        bool explicit_gc_requested = _gc_requested.is_set() &&  is_explicit_gc(_requested_gc_cause);
        // Choose which GC mode to run in. The block below should select a single mode.
        GCMode mode = none;

        if (explicit_gc_requested) {
             //  java_lang_system_gc
             cause = _requested_gc_cause;
             log_info(gc)("Trigger: Explicit GC request (%s)", GCCause::to_string(cause));
              // -XX:+ExplicitGCInvokesConcurrent
              if (ExplicitGCInvokesConcurrent) {
                    policy->record_explicit_to_concurrent();
                    // concurrent_normal 併發模式
                    mode = default_mode;
              } else {
                    policy->record_explicit_to_full();
                    mode = stw_full; // Full GC 模式
              }
        }

      switch (mode) {
        case concurrent_normal:
          // 由 concurrent gc 執行緒非同步執行
          service_concurrent_normal_cycle(cause);
          break;
        case stw_full:
          // 觸發 VM_ShenandoahFullGC ,由 VMthread 同步執行
          service_stw_full_cycle(cause);
          break;
        default:
          ShouldNotReachHere();
      }

      // If this was the requested GC cycle, notify waiters about it
      if (explicit_gc_requested || implicit_gc_requested) {
        // 喚醒在 handle_requested_gc 中阻塞等待的 java 執行緒
        notify_gc_waiters();
      }
  }
}

相關文章