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

来源:https://www.cnblogs.com/binlovetech/p/18109260
-Advertisement-
Play Games

本文基於 OpenJDK17 進行討論 在 JDK NIO 針對堆外記憶體的分配場景中,我們經常會看到 System.gc 的身影,比如當我們通過 FileChannel#map 對文件進行記憶體映射的時候,如果 JVM 進程虛擬記憶體空間中的虛擬記憶體不足,JVM 在 native 層就會拋出 OutOf ...


本文基於 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();
      }
  }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.需求描述 根據項目需求,採用Antd組件庫需要封裝一個評論框,具有以下功能: 支持文字輸入 支持常用表情包選擇 支持發佈評論 支持自定義表情包 2.封裝代碼 ./InputComment.tsx 1 import React, { useState, useRef, useEffect, for ...
  • 需要實現的效果: 1、子菜單如果不是全部選中,一級菜單半選。 2、子菜單全選,一級菜單選中。 3、一級菜單選擇,二級菜單全選。 4、沒有二級菜單,則只控制一級菜單。 主要用到的屬性是checked和halfCheckedKeys,通過手動控制那些菜單選中,那些半選中實現功能。 **頁面截圖: ** ...
  • 一、HTTP1.0 HTTP協議的第二個版本,第一個在通訊中指定版本號的HTTP協議版本 HTTP 1.0 瀏覽器與伺服器只保持短暫的連接,每次請求都需要與伺服器建立一個TCP連接 伺服器完成請求處理後立即斷開TCP連接,伺服器不跟蹤每個客戶也不記錄過去的請求 簡單來講,每次與伺服器交互,都需要新開 ...
  • Ajax 與 Axios 非同步請求 一、伺服器對外提供了哪些資源 1. 網頁中如何請求數據 數據,也是伺服器對外提供的一種資源。只要是資源,必然要通過 請求 – 處理 – 響應 的方式進行獲取。如果要在網頁中請求伺服器上的數據資源,則需要用到 XMLHttpRequest 對象。XMLHttpReq ...
  • 網頁圖像漸變的方法(HTML+CSS)(漸變與切換) Date: 2024.03.27 參考 色彩 runoob-漸變色工具 漸變 - 水平多圖 效果 HTML <div class="conBox pubCon"> <div class="imgBox"> <img class="img1" sr ...
  • 客戶管理系統的應用架構設計 應用層定義了軟體系統的應用功能,負責接收用戶的請求,協調領域層能力來執行任務,並將結果返回給用戶,功能模塊包括: 客戶管理:核心功能模塊,負責收集和更新客戶信息,包括個人資料、聯繫方式、消費習慣、會員卡、歸屬信息(比如銷售或顧問)和備註。這個模塊是CRM系統的基礎,支撐其 ...
  • C++ 數學 C++ 有許多函數可以讓您在數字上執行數學任務。 最大值和最小值 max(x, y) 函數可用於找到 x 和 y 的最大值: 示例 cout << max(5, 10); 而 min(x, y) 函數可用於找到 x 和 y 的最小值: 示例 cout << min(5, 10); C+ ...
  • 1 枚舉好用嗎? 數據字典型欄位,枚舉比Integer好: 限定值,只能賦值枚舉的那幾個實例,不能像Integer隨便輸,保存和查詢的時候特別有用 含義明確,使用時不需要去查數據字典 顯示值跟存儲值直接映射,不需要手動轉換,比如1在頁面上顯示為啟用,0顯示禁用,枚舉定義好可以直接顯示 基於enum可 ...
一周排行
    -Advertisement-
    Play Games
  • 隨著Aspire發佈preview5的發佈,Microsoft.Extensions.ServiceDiscovery隨之更新, 服務註冊發現這個屬於老掉牙的話題解決什麼問題就不贅述了,這裡主要講講Microsoft.Extensions.ServiceDiscovery(preview5)以及如何 ...
  • 概述:通過使用`SemaphoreSlim`,可以簡單而有效地限制非同步HTTP請求的併發量,確保在任何給定時間內不超過20個網頁同時下載。`ParallelOptions`不適用於非同步操作,但可考慮使用`Parallel.ForEach`,儘管在非同步場景中謹慎使用。 對於併發非同步 I/O 操作的數量 ...
  • 1.Linux上安裝Docken 伺服器系統版本以及內核版本:cat /etc/redhat-release 查看伺服器內核版本:uname -r 安裝依賴包:yum install -y yum-utils device-mapper-persistent-data lvm2 設置阿裡雲鏡像源:y ...
  • 概述:WPF界面綁定和渲染大量數據可能導致性能問題。通過啟用UI虛擬化、非同步載入和數據分頁,可以有效提高界面響應性能。以下是簡單示例演示這些優化方法。 在WPF中,當你嘗試綁定和渲染大量的數據項時,性能問題可能出現。以下是一些可能導致性能慢的原因以及優化方法: UI 虛擬化: WPF提供了虛擬化技術 ...
  • 引言 上一章節介紹了 TDD 的三大法則,今天我們講一下在單元測試中模擬對象的使用。 Fake Fake - Fake 是一個通用術語,可用於描述 stub或 mock 對象。 它是 stub 還是 mock 取決於使用它的上下文。 也就是說,Fake 可以是 stub 或 mock Mock - ...
  • 為.net6在CentOS7上面做準備,先在vmware虛擬機安裝CentOS 7.9 新建CentOS764位的系統 因為CentOS8不更新了,所以安裝7;簡單就一筆帶過了 選擇下載好的操作系統的iso文件,下載地址https://mirrors.aliyun.com/centos/7.9.20 ...
  • 經過前面幾篇的學習,我們瞭解到指令的大概分類,如:參數載入指令,該載入指令以 Ld 開頭,將參數載入到棧中,以便於後續執行操作命令。參數存儲指令,其指令以 St 開頭,將棧中的數據,存儲到指定的變數中,以方便後續使用。創建實例指令,其指令以 New 開頭,用於在運行時動態生成並初始化對象。方法調用指... ...
  • LiteDB 是一個輕量級的嵌入式 NoSQL 資料庫,其設計理念與 MongoDB 類似,但它是完全使用 C# 開發的,因此與 C# 應用程式的集成非常順暢。與 SQLite 相比,LiteDB 提供了 NoSQL(即鍵值對)的數據存儲方式,並且是一個開源且免費的項目。它適用於桌面、移動以及 We ...
  • 1 開源解析和拆分文檔 第三方的工具去對文件解析拆分,去將我們的文件內容給提取出來,並將我們的文檔內容去拆分成一個小的chunk。常見的PDF word mark down, JSON、HTML。都可以有很好的一些模塊去把這些文件去進行一個東西去提取。 優勢 支持豐富的文檔類型 每種文檔多樣化選擇 ...
  • OOM是什麼?英文全稱為 OutOfMemoryError(記憶體溢出錯誤)。當程式發生OOM時,如何去定位導致異常的代碼還是挺麻煩的。 要檢查OOM發生的原因,首先需要瞭解各種OOM情況下會報的異常信息。這樣能縮小排查範圍,再結合異常堆棧、heapDump文件、JVM分析工具和業務代碼來判斷具體是哪 ...