本文基於 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 堆中的記憶體占用,但這部分記憶體占用很小,就相當於是冰山的一角。
而位於冰山下麵的大一片 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();
}
}
}