本文基於 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_gc
,GCCause::_dcmd_gc_run
,GCCause::_jvmti_force_gc
,GCCause::_heap_inspection
,GCCause::_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
替代
如何靈活可控的主動觸發各種 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: