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

本文基於 Java 17-ea,可是相關設計在 Java 11 以後是大體同樣的java

咱們常常在面試中詢問 System.gc() 究竟會不會馬上觸發 Full GC,網上也有不少人給出了答案,可是這些答案都有些過期了。本文基於最新的 Java 的下一個即將發佈的 LTS 版本 Java 17(ea)的源代碼,深刻解析 System.gc() 背後的故事。git

爲何須要System.gc()

1. 使用並管理堆外內存的框架,須要 Full GC 的機制觸發堆外內存回收

JVM 的內存,不止堆內存,還有其餘不少塊,經過 Native Memory Tracking 能夠看到:github

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()app

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

有些時候,咱們爲了測試,學習 JVM 的某些機制,須要讓 JVM 作一次 GC 以後開始,這也會用到System.gc()。可是其實有更好的方法,後面你會看到。框架

System.gc() 背後的原理

System.gc()實際上調用的是RunTime.getRunTime().gc():jvm

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

這個方法是一個 native 方法:async

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

相關文章
相關標籤/搜索