JVM相關 - 深入理解 System.gc()

乾貨滿滿張雜湊發表於2021-02-25

本文基於 Java 17-ea,但是相關設計在 Java 11 之後是大致一樣的

我們經常在面試中詢問 System.gc() 究竟會不會立刻觸發 Full GC,網上也有很多人給出了答案,但是這些答案都有些過時了。本文基於最新的 Java 的下一個即將釋出的 LTS 版本 Java 17(ea)的原始碼,深入解析 System.gc() 背後的故事。

為什麼需要System.gc()

1. 使用並管理堆外記憶體的框架,需要 Full GC 的機制觸發堆外記憶體回收

JVM 的記憶體,不止堆記憶體,還有其他很多塊,通過 Native Memory Tracking 可以看到:

Native Memory Tracking:

Total: reserved=6308603KB, committed=4822083KB
-                 Java Heap (reserved=4194304KB, committed=4194304KB)
                            (mmap: reserved=4194304KB, committed=4194304KB) 
 
-                     Class (reserved=1161041KB, committed=126673KB)
                            (classes #21662)
                            (  instance classes #20542, array classes #1120)
                            (malloc=3921KB #64030) 
                            (mmap: reserved=1157120KB, committed=122752KB) 
                            (  Metadata:   )
                            (    reserved=108544KB, committed=107520KB)
                            (    used=105411KB)
                            (    free=2109KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=15232KB)
                            (    used=13918KB)
                            (    free=1314KB)
                            (    waste=0KB =0.00%)
 
-                    Thread (reserved=355251KB, committed=86023KB)
                            (thread #673)
                            (stack: reserved=353372KB, committed=84144KB)
                            (malloc=1090KB #4039) 
                            (arena=789KB #1344)
 
-                      Code (reserved=252395KB, committed=69471KB)
                            (malloc=4707KB #17917) 
                            (mmap: reserved=247688KB, committed=64764KB) 
 
-                        GC (reserved=199635KB, committed=199635KB)
                            (malloc=11079KB #29639) 
                            (mmap: reserved=188556KB, committed=188556KB) 
 
-                  Compiler (reserved=2605KB, committed=2605KB)
                            (malloc=2474KB #2357) 
                            (arena=131KB #5)
 
-                  Internal (reserved=3643KB, committed=3643KB)
                            (malloc=3611KB #8683) 
                            (mmap: reserved=32KB, committed=32KB) 
 
-                     Other (reserved=67891KB, committed=67891KB)
                            (malloc=67891KB #2859) 
 
-                    Symbol (reserved=26220KB, committed=26220KB)
                            (malloc=22664KB #292684) 
                            (arena=3556KB #1)
 
-    Native Memory Tracking (reserved=7616KB, committed=7616KB)
                            (malloc=585KB #8238) 
                            (tracking overhead=7031KB)
 
-               Arena Chunk (reserved=10911KB, committed=10911KB)
                            (malloc=10911KB) 
 
-                   Tracing (reserved=25937KB, committed=25937KB)
                            (malloc=25937KB #8666) 
 
-                   Logging (reserved=5KB, committed=5KB)
                            (malloc=5KB #196) 
 
-                 Arguments (reserved=18KB, committed=18KB)
                            (malloc=18KB #486) 
 
-                    Module (reserved=532KB, committed=532KB)
                            (malloc=532KB #3579) 
 
-              Synchronizer (reserved=591KB, committed=591KB)
                            (malloc=591KB #4777) 
 
-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB) 

  • Java Heap: 堆記憶體,即-Xmx限制的最大堆大小的記憶體。
  • Class:載入的類與方法資訊,其實就是 metaspace,包含兩部分: 一是 metadata,被-XX:MaxMetaspaceSize限制最大大小,另外是 class space,被-XX:CompressedClassSpaceSize限制最大大小
  • Thread:執行緒與執行緒棧佔用記憶體,每個執行緒棧佔用大小受-Xss限制,但是總大小沒有限制。
  • Code:JIT 即時編譯後(C1 C2 編譯器優化)的程式碼佔用記憶體,受 -XX:ReservedCodeCacheSize限制
  • GC:垃圾回收佔用記憶體,例如垃圾回收需要的 CardTable,標記數,區域劃分記錄,還有標記 GC Root 等等,都需要記憶體。這個不受限制,一般不會很大的。
  • Compiler:C1 C2 編譯器本身的程式碼和標記佔用的記憶體,這個不受限制,一般不會很大的
  • Internal:命令列解析,JVMTI 使用的記憶體,這個不受限制,一般不會很大的
  • Symbol: 常量池佔用的大小,字串常量池受-XX:StringTableSize 個數限制,總記憶體大小不受限制
  • Native Memory Tracking:記憶體採集本身佔用的記憶體大小,如果沒有開啟採集(那就看不到這個了,哈哈),就不會佔用,這個不受限制,一般不會很大的
  • Arena Chunk:所有通過 arena 方式分配的記憶體,這個不受限制,一般不會很大的
  • Tracing:所有采集佔用的記憶體,如果開啟了 JFR 則主要是 JFR 佔用的記憶體。這個不受限制,一般不會很大的
  • Logging,Arguments,Module,Synchronizer,Safepoint,Other,這些一般我們不會關心。

除了 Native Memory Tracking 記錄的記憶體使用,還有兩種記憶體 Native Memory Tracking 沒有記錄,那就是:

  • Direct Buffer:直接記憶體
  • MMap Buffer:檔案對映記憶體

針對除了堆記憶體以外,其他的記憶體,有些也是需要 GC 的。例如:MetaSpace,CodeCache,Direct Buffer,MMap Buffer 等等。早期在 Java 8 之前的 JVM,對於這些記憶體回收的機制並不完善,很多情況下都需要 FullGC 掃描整個堆才能確定這些區域中哪些記憶體可以回收。

有一些框架,大量使用並管理了這些堆外空間。例如 netty 使用了 Direct Buffer,Kafka 和 RocketMQ 使用了 Direct Buffer 和 MMap Buffer。他們都是提前從系統申請好一塊記憶體,之後管理起來並使用。在空間不足時,繼續向系統申請,並且也會有縮容。例如 netty,在使用的 Direct Buffer 達到-XX:MaxDirectMemorySize的限制之後,則會先嚐試將不可達的Reference物件加入Reference連結串列中,依賴Reference的內部守護執行緒觸發可以被回收DirectByteBuffer關聯的Cleaner的run()方法。如果記憶體還是不足, 則執行System.gc(),期望觸發full gc,來回收堆記憶體中的DirectByteBuffer物件來觸發堆外記憶體回收,如果還是超過限制,則丟擲java.lang.OutOfMemoryError.

2. 使用了 WeakReference, SoftReference 的程式,需要相應的 GC 回收。

對於 WeakReference,只要發生 GC,無論是 Young GC 還是 FullGC 就會被回收。SoftReference 只有在 FullGC 的時候才會被回收。當我們程式想主動對於這些引用進行回收的時候,需要能觸發 GC 的方法,這就用到了System.gc()

3. 測試,學習 JVM 機制的時候

有些時候,我們為了測試,學習 JVM 的某些機制,需要讓 JVM 做一次 GC 之後開始,這也會用到System.gc()。但是其實有更好的方法,後面你會看到。

System.gc() 背後的原理

System.gc()實際上呼叫的是RunTime.getRunTime().gc():

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

這個方法是一個 native 方法:

public native void gc();

對應 JVM 原始碼:

JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  JVMWrapper("JVM_GC");
  //如果沒有將JVM啟動引數 DisableExplicitGC 設定為 false,則執行 GC,GC 原因是 System.gc 觸發,對應 GCCause::_java_lang_system_gc
  if (!DisableExplicitGC) {
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
  }
JVM_END

首先,根據 DisableExplicitGC 這個 JVM 啟動引數的狀態,確定是否會 GC,如果需要 GC,不同 GC 會有不同的處理。

1. G1 GC 的處理

如果是 System.gc() 觸發的 GC,G1 GC 會根據 ExplicitGCInvokesConcurrent 這個 JVM 引數決定是預設 GC (輕量 GC,YoungGC)還是 FullGC。

參考程式碼g1CollectedHeap.cpp

//是否應該並行 GC,也就是較為輕量的 GC,對於 GCCause::_java_lang_system_gc,這裡就是判斷 ExplicitGCInvokesConcurrent 這個 JVM 是否為 true
if (should_do_concurrent_full_gc(cause)) {
    return try_collect_concurrently(cause,
                                    gc_count_before,
                                    old_marking_started_before);
}// 省略其他這裡我們不關心的判斷分支
 else {
    //否則進入 full GC
    VM_G1CollectFull op(gc_count_before, full_gc_count_before, cause);
    VMThread::execute(&op);
    return op.gc_succeeded();
}

2. ZGC 的處理

直接不處理,不支援通過 System.gc() 觸發 GC。

參考原始碼:zDriver.cpp

void ZDriver::collect(GCCause::Cause cause) {
  switch (cause) {
  //注意這裡的 _wb 開頭的 GC 原因,這代表是 WhiteBox 觸發的,後面我們會用到,這裡先記一下
  case GCCause::_wb_young_gc:
  case GCCause::_wb_conc_mark:
  case GCCause::_wb_full_gc:
  case GCCause::_dcmd_gc_run:
  case GCCause::_java_lang_system_gc:
  case GCCause::_full_gc_alot:
  case GCCause::_scavenge_alot:
  case GCCause::_jvmti_force_gc:
  case GCCause::_metadata_GC_clear_soft_refs:
    // Start synchronous GC
    _gc_cycle_port.send_sync(cause);
    break;

  case GCCause::_z_timer:
  case GCCause::_z_warmup:
  case GCCause::_z_allocation_rate:
  case GCCause::_z_allocation_stall:
  case GCCause::_z_proactive:
  case GCCause::_z_high_usage:
  case GCCause::_metadata_GC_threshold:
    // Start asynchronous GC
    _gc_cycle_port.send_async(cause);
    break;

  case GCCause::_gc_locker:
    // Restart VM operation previously blocked by the GC locker
    _gc_locker_port.signal();
    break;

  case GCCause::_wb_breakpoint:
    ZBreakpoint::start_gc();
    _gc_cycle_port.send_async(cause);
    break;

  //對於其他原因,不觸發GC,GCCause::_java_lang_system_gc 會走到這裡
  default:
    // Other causes not supported
    fatal("Unsupported GC cause (%s)", GCCause::to_string(cause));
    break;
  }
}

3. Shenandoah GC 的處理

Shenandoah 的處理和 G1 GC 的類似,先判斷是不是使用者明確觸發的 GC,然後通過 DisableExplicitGC 這個 JVM 引數判斷是否可以 GC(其實這個是多餘的,可以去掉,因為外層JVM_ENTRY_NO_ENV(void, JVM_GC(void))已經處理這個狀態位了)。如果可以,則請求 GC,阻塞等待 GC 請求被處理。然後根據 ExplicitGCInvokesConcurrent 這個 JVM 引數決定是預設 GC (輕量並行 GC,YoungGC)還是 FullGC

參考原始碼shenandoahControlThread.cpp

void ShenandoahControlThread::request_gc(GCCause::Cause cause) {
  assert(GCCause::is_user_requested_gc(cause) ||
         GCCause::is_serviceability_requested_gc(cause) ||
         cause == GCCause::_metadata_GC_clear_soft_refs ||
         cause == GCCause::_full_gc_alot ||
         cause == GCCause::_wb_full_gc ||
         cause == GCCause::_scavenge_alot,
         "only requested GCs here");
  //如果是顯式GC(即如果是GCCause::_java_lang_system_gc,GCCause::_dcmd_gc_run,GCCause::_jvmti_force_gc,GCCause::_heap_inspection,GCCause::_heap_dump中的任何一個)
  if (is_explicit_gc(cause)) {
    //如果沒有關閉顯式GC,也就是 DisableExplicitGC 為 false
    if (!DisableExplicitGC) {
      //請求 GC
      handle_requested_gc(cause);
    }
  } else {
    handle_requested_gc(cause);
  }
}

請求 GC 的程式碼流程是:

void ShenandoahControlThread::handle_requested_gc(GCCause::Cause cause) {
  MonitorLocker ml(&_gc_waiters_lock);
  //獲取當前全域性 GC id
  size_t current_gc_id = get_gc_id();
  //因為要進行 GC ,所以將id + 1
  size_t required_gc_id = current_gc_id + 1;
  //直到當前全域性 GC id + 1 為止,代表 GC 執行了
  while (current_gc_id < required_gc_id) {
    //設定 gc 狀態位,會有其他執行緒掃描執行 gc
    _gc_requested.set();
    //記錄 gc 原因,根據不同原因有不同的處理策略,我們這裡是 GCCause::_java_lang_system_gc
    _requested_gc_cause = cause;
    //等待 gc 鎖物件 notify,代表 gc 被執行並完成
    ml.wait();
    current_gc_id = get_gc_id();
  }
}

對於GCCause::_java_lang_system_gc,GC 的執行流程大概是:

bool explicit_gc_requested = _gc_requested.is_set() &&  is_explicit_gc(_requested_gc_cause);

//省略一些程式碼

else if (explicit_gc_requested) {
  cause = _requested_gc_cause;
  log_info(gc)("Trigger: Explicit GC request (%s)", GCCause::to_string(cause));

  heuristics->record_requested_gc();
  // 如果 JVM 引數 ExplicitGCInvokesConcurrent 為 true,則走預設輕量 GC
  if (ExplicitGCInvokesConcurrent) {
    policy->record_explicit_to_concurrent();
    mode = default_mode;
    // Unload and clean up everything
    heap->set_unload_classes(heuristics->can_unload_classes());
  } else {
    //否則,執行 FullGC
    policy->record_explicit_to_full();
    mode = stw_full;
  }
}

System.gc() 相關的 JVM 引數

1. DisableExplicitGC

說明:是否禁用顯式 GC,預設是不禁用的。對於 Shenandoah GC,顯式 GC 包括:GCCause::_java_lang_system_gcGCCause::_dcmd_gc_runGCCause::_jvmti_force_gcGCCause::_heap_inspectionGCCause::_heap_dump,對於其他 GC,僅僅限制GCCause::_java_lang_system_gc

預設:false

舉例:如果想禁用顯式 GC:-XX:+DisableExplicitGC

2. ExplicitGCInvokesConcurrent

說明:對於顯式 GC,是執行輕量並行 GC (YoungGC)還是 FullGC,如果為 true 則是執行輕量並行 GC (YoungGC),false 則是執行 FullGC

預設:false

舉例:啟用的話指定:-XX:+ExplicitGCInvokesConcurrent

其實,在設計上有人提出(參考連結)想將 ExplicitGCInvokesConcurrent 改為 true。但是目前並不是所有的 GC 都可以在輕量並行 GC 對 Java 所有記憶體區域進行回收,有些時候必須通過 FullGC。所以,目前這個引數還是預設為 false

3. 已過期的 ExplicitGCInvokesConcurrentAndUnloads 和使用 ClassUnloadingWithConcurrentMark 替代

如果顯式 GC採用輕量並行 GC,那麼無法執行 Class Unloading(類解除安裝),如果啟用了類解除安裝功能,可能會有異常。所以通過這個狀態位來標記在顯式 GC時,即使採用輕量並行 GC,也要掃描進行類解除安裝。
ExplicitGCInvokesConcurrentAndUnloads目前已經過期了,用ClassUnloadingWithConcurrentMark替代

參考BUG-JDK-8170388

如何靈活可控的主動觸發各種 GC?

答案是通過 WhiteBox API。但是這個不要在生產上面執行,僅僅用來測試 JVM 還有學習 JVM 使用。WhiteBox API 是 HotSpot VM 自帶的白盒測試工具,將內部的很多核心機制的 API 暴露出來,用於白盒測試 JVM,壓測 JVM 特性,以及輔助學習理解 JVM 並調優引數。WhiteBox API 是 Java 7 引入的,目前 Java 8 LTS 以及 Java 11 LTS(其實是 Java 9+ 以後的所有版本,這裡只關心 LTS 版本,Java 9 引入了模組化所以 WhiteBox API 有所變化)都是有的。但是預設這個 API 並沒有編譯在 JDK 之中,但是他的實現是編譯在了 JDK 裡面了。所以如果想用這個 API,需要使用者自己編譯需要的 API,並加入 Java 的 BootClassPath 並啟用 WhiteBox API。下面我們來用 WhiteBox API 來主動觸發各種 GC。

1. 編譯 WhiteBox API

https://github.com/openjdk/jdk/tree/master/test/lib路徑下的sun目錄取出,編譯成一個 jar 包,名字假設是 whitebox.jar

2. 編寫測試程式

whitebox.jar 新增到你的專案依賴,之後寫程式碼

public static void main(String[] args) throws Exception {
        WhiteBox whiteBox = WhiteBox.getWhiteBox();
        //執行young GC
        whiteBox.youngGC();
        System.out.println("---------------------------------");
        whiteBox.fullGC();
        //執行full GC
        whiteBox.fullGC();
        //保持程式不退出,保證日誌列印完整
        Thread.currentThread().join();
}

3. 啟動程式檢視效果

使用啟動引數 -Xbootclasspath/a:/home/project/whitebox.jar -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xlog:gc 啟動程式。其中前三個 Flag 表示啟用 WhiteBox API,最後一個表示列印 GC info 級別的日誌到控制檯。

我的輸出:

[0.036s][info][gc] Using G1
[0.048s][info][gc,init] Version: 17-internal+0-adhoc.Administrator.jdk (fastdebug)
[0.048s][info][gc,init] CPUs: 16 total, 16 available
[0.048s][info][gc,init] Memory: 16304M
[0.048s][info][gc,init] Large Page Support: Disabled
[0.048s][info][gc,init] NUMA Support: Disabled
[0.048s][info][gc,init] Compressed Oops: Enabled (32-bit)
[0.048s][info][gc,init] Heap Region Size: 1M
[0.048s][info][gc,init] Heap Min Capacity: 512M
[0.048s][info][gc,init] Heap Initial Capacity: 512M
[0.048s][info][gc,init] Heap Max Capacity: 512M
[0.048s][info][gc,init] Pre-touch: Disabled
[0.048s][info][gc,init] Parallel Workers: 13
[0.048s][info][gc,init] Concurrent Workers: 3
[0.048s][info][gc,init] Concurrent Refinement Workers: 13
[0.048s][info][gc,init] Periodic GC: Disabled
[0.049s][info][gc,metaspace] CDS disabled.
[0.049s][info][gc,metaspace] Compressed class space mapped at: 0x0000000100000000-0x0000000140000000, reserved size: 1073741824
[0.049s][info][gc,metaspace] Narrow klass base: 0x0000000000000000, Narrow klass shift: 3, Narrow klass range: 0x140000000
[1.081s][info][gc,start    ] GC(0) Pause Young (Normal) (WhiteBox Initiated Young GC)
[1.082s][info][gc,task     ] GC(0) Using 12 workers of 13 for evacuation
[1.089s][info][gc,phases   ] GC(0)   Pre Evacuate Collection Set: 0.5ms
[1.089s][info][gc,phases   ] GC(0)   Merge Heap Roots: 0.1ms
[1.089s][info][gc,phases   ] GC(0)   Evacuate Collection Set: 3.4ms
[1.089s][info][gc,phases   ] GC(0)   Post Evacuate Collection Set: 1.6ms
[1.089s][info][gc,phases   ] GC(0)   Other: 1.3ms
[1.089s][info][gc,heap     ] GC(0) Eden regions: 8->0(23)
[1.089s][info][gc,heap     ] GC(0) Survivor regions: 0->2(4)
[1.089s][info][gc,heap     ] GC(0) Old regions: 0->0
[1.089s][info][gc,heap     ] GC(0) Archive regions: 0->0
[1.089s][info][gc,heap     ] GC(0) Humongous regions: 0->0
[1.089s][info][gc,metaspace] GC(0) Metaspace: 6891K(7104K)->6891K(7104K) NonClass: 6320K(6400K)->6320K(6400K) Class: 571K(704K)->571K(704K)
[1.089s][info][gc          ] GC(0) Pause Young (Normal) (WhiteBox Initiated Young GC) 7M->1M(512M) 7.864ms
[1.089s][info][gc,cpu      ] GC(0) User=0.00s Sys=0.00s Real=0.01s
---------------------------------
[1.091s][info][gc,task     ] GC(1) Using 12 workers of 13 for full compaction
[1.108s][info][gc,start    ] GC(1) Pause Full (WhiteBox Initiated Full GC)
[1.108s][info][gc,phases,start] GC(1) Phase 1: Mark live objects
[1.117s][info][gc,phases      ] GC(1) Phase 1: Mark live objects 8.409ms
[1.117s][info][gc,phases,start] GC(1) Phase 2: Prepare for compaction
[1.120s][info][gc,phases      ] GC(1) Phase 2: Prepare for compaction 3.031ms
[1.120s][info][gc,phases,start] GC(1) Phase 3: Adjust pointers
[1.126s][info][gc,phases      ] GC(1) Phase 3: Adjust pointers 5.806ms
[1.126s][info][gc,phases,start] GC(1) Phase 4: Compact heap
[1.190s][info][gc,phases      ] GC(1) Phase 4: Compact heap 63.812ms
[1.193s][info][gc,heap        ] GC(1) Eden regions: 1->0(25)
[1.193s][info][gc,heap        ] GC(1) Survivor regions: 2->0(4)
[1.193s][info][gc,heap        ] GC(1) Old regions: 0->3
[1.193s][info][gc,heap        ] GC(1) Archive regions: 0->0
[1.193s][info][gc,heap        ] GC(1) Humongous regions: 0->0
[1.193s][info][gc,metaspace   ] GC(1) Metaspace: 6895K(7104K)->6895K(7104K) NonClass: 6323K(6400K)->6323K(6400K) Class: 571K(704K)->571K(704K)
[1.193s][info][gc             ] GC(1) Pause Full (WhiteBox Initiated Full GC) 1M->0M(512M) 84.846ms
[1.202s][info][gc,cpu         ] GC(1) User=0.19s Sys=0.63s Real=0.11s

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

image

相關文章