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


本文基於 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM