【易錯問題】Major GC和Full GC的區別是什麼?觸發條件呢?
相信大多數人的理解是Major GC只針對老年代,Full GC會先觸發一次Minor GC,不知對否?我參考了R大的分析和介紹,總結了一下相關的說明和分析結論。
在基於HotSpotVM的基礎角度
針對HotSpot VM的實現,它裡面的GC其實準確分類只有兩大種:
Partial GC(部分回收模式)
Partial GC代表著並不收集整個GC堆的模式
- Young Generation GC(新生代回收模式):它主要是進行回收新生代範圍內的記憶體物件的GC回收器。
- Old/Tenured Generation GC(老年代回收模式):它主要是針對於回收老年代Old/Tenured Generation範圍內的GC垃圾回收器(CMS的Concurrent Collection是這個模式)。
- Mixed Generation GC(混合代回收模式):收集整個young gen以及部分old gen的GC。只有G1有這個模式
Full GC(全體回收模式)
Full GC代表著收集整個JVM的執行時堆+方法區+直接堆外記憶體的總體範圍內。(甚至可以理解為JVM程式範圍內的絕大部分範圍的資料區域)。
它會涵蓋了所有的模式和區域包含:Young Gen(新生代)、Tenured Gen(老生代)、Perm/Meta Gen(元空間)(JDK8前後的版本)等全域性範圍的GC垃圾回收模式。
在一般情況下Major GC通常是跟Full GC是等價的,收集整個GC堆。但如果從HotSpot VM底層的細節出發,如果再有人說“Major GC”的時候一定要問清楚他想要指的是上面的Full GC還是Old/Tenured GC。
基於最簡單的分代式GC策略
觸發條件是:Young GC
按HotSpot VM的Serial GC的實現來看,當Young gen中的Eden區分達到閾值(屬於一定的百分比進行控制)的時候觸發。
注意:Young GC中有部分存活物件會晉升到Old/Tenured Gen,所以Young GC後Old Gen的佔用量通常會有所升高。
觸發條件是:Full GC
-
當準備要觸發一次Young GC時,如果發現統計資料說之前Young Old/Tenured Gen剩餘的空間大,則不會觸發Young GC,而是轉為觸發Full GC(因為HotSpot VM的GC裡,除了CMS的Concurrent collection之外,其它能收集Old/Tenured Gen的GC都會同時收集整個GC堆,包括Young gen,所以不需要事先觸發一次單獨的Young GC);
-
如果有Perm/Meta gen的話,要在Perm/Meta gen分配空間但已經沒有足夠空間時,也要觸發一次full GC。
-
System.gc()方法或者Heap Dump自帶的GC,預設也是觸發Full GC。HotSpot VM裡其它非併發GC的觸發條件複雜一些,不過大致的原理與上面說的其實一樣。
注意:Parallel Scavenge(-XX:+UseParallelGC)框架下,預設是在要觸發Full GC前先執行一次Young GC,並且兩次GC之間能讓應用程式稍微執行一小下,以期降低Full GC的暫停時間(因為young GC會盡量清理了Young Gen的垃圾物件,減少了Full GC的掃描工作量)。控制這個行為的VM引數是-XX:+ScavengeBeforeFullGC。
觸發條件是:Concurrent GC
Concurrent GC的觸發條件就不太一樣。以CMS GC為例,它主要是定時去檢查Old Gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對Old gen做併發收集。
GC回收器對應的GC模式列舉
在Hotspot JVM實現的Serial GC, Parallel GC, CMS, G1 GC中大致可以對應到某個Young GC和Old GC演算法組合;
- Serial GC演算法:Serial Young GC + Serial Old GC (實際上它是全域性範圍的Full GC);
- Parallel GC演算法:Parallel Young GC + 非並行的PS MarkSweep GC / 並行的Parallel Old GC(這倆實際上也是全域性範圍的Full GC),選PS MarkSweep GC 還是 Parallel Old GC 由引數UseParallelOldGC來控制;
- CMS演算法:ParNew(Young)GC + CMS(Old)GC (piggyback on ParNew的結果/老生代存活下來的object只做記錄,不做compaction)+ Full GC for CMS演算法(應對核心的CMS GC某些時候的不趕趟,開銷很大);
- G1 GC:Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC for G1 GC演算法(應對G1 GC演算法某些時候的不趕趟,開銷很大);
GC回收模式的觸發總結
- 搞清楚了上面這些組合,我們再來看看各類GC演算法的觸發條件。簡單說,觸發條件就是某GC演算法對應區域滿了,或是預測快滿了。比如,
- 各種Young GC的觸發原因都是eden區滿了;
- Serial Old GC/PS MarkSweep GC/Parallel Old GC的觸發則是在要執行Young GC時候預測其promote的object的總size超過老生代剩餘size;
- CMS GC的initial marking的觸發條件是老生代使用比率超過某值;
- G1 GC的initial marking的觸發條件是Heap使用比率超過某值;
- Full GC for CMS演算法和Full GC for G1 GC演算法的觸發原因很明顯,就是4.3 和 4.4 的fancy演算法不趕趟了,只能全域性範圍大搞一次GC了(相信我,這很慢!這很慢!這很慢!);
【坑點與坑點】-XX:+DisableExplicitGC 與 NIO的direct memory的關係
很多人都見過JVM調優建議裡使用這個引數,對吧?但是為什麼要用它,什麼時候應該用而什麼時候用了會掉坑裡呢?
-
首先,要了解的是這個引數的作用。在Oracle/Sun JDK這個具體實現上,System.gc()的預設效果是引發一次stop-the-world的Full GC,由上面所知就是針對於整個GC堆做記憶體垃圾收集。
-
再次,如果採用了用了-XX:+DisableExplicitGC引數後,System.gc()的呼叫就會變成一個空呼叫,完全不會觸發任何GC(但是“函式呼叫”本身的開銷還是存在的哦~)。
- 為啥要用這個引數呢?最主要的原因是為了防止某些小白同學在程式碼裡到處寫System.gc()的呼叫而干擾了程式的正常執行吧。
- 有些應用程式本來可能正常跑一天也不會出一次Full GC,但就是因為有人在程式碼裡呼叫了System.gc()而不得不間歇性被暫停。
- 有些時候這些呼叫是在某些庫或框架裡寫的,改不了它們的程式碼但又不想被這些呼叫干擾也會用這引數。
- 為啥要用這個引數呢?最主要的原因是為了防止某些小白同學在程式碼裡到處寫System.gc()的呼叫而干擾了程式的正常執行吧。
-XX:+DisableExplicitGC看起來這引數應該總是開著嘛。有啥坑呢?
下述三個條件同時滿足時會發生的
- 應用本身在GC堆內的物件行為良好,正常情況下很久都不發生Full GC。
- 應用大量使用了NIO的direct memory,經常、反覆的申請DirectByteBuffer。
- 使用了-XX:+DisableExplicitGC。
能觀察到的現象是:
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:633)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:98)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
用一個案例來分析這現象:
import java.nio.*;
public class DisableExplicitGCDemo {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
ByteBuffer.allocateDirect(128);
}
System.out.println("Done");
}
}
然後編譯、執行。
$ java -version
java version "1.6.0_25"
Java(TM) SE Runtime Environment (build 1.6.0_25-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.0-b11, mixed mode)
$ javac DisableExplicitGCDemo.java
$ java -XX:MaxDirectMemorySize=10m -XX:+PrintGC -XX:+DisableExplicitGC DisableExplicitGCDemo
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:633)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:98)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
at DisableExplicitGCDemo.main(DisableExplicitGCDemo.java:6)
$ java -XX:MaxDirectMemorySize=10m -XX:+PrintGC DisableExplicitGCDemo
[GC 10996K->10480K(120704K), 0.0433980 secs]
[Full GC 10480K->10415K(120704K), 0.0359420 secs]
Done
-
可以看到,同樣的程式,不帶
-XX:+DisableExplicitGC
時能正常完成執行,而帶上這個引數後卻出現了OOM。-XX:MaxDirectMemorySize=10m限制了DirectByteBuffer能分配的空間的限額,以便問題更容易展現出來。不用這個引數就得多跑一會兒了。 -
迴圈不斷申請DirectByteBuffer但並沒有引用,所以這些DirectByteBuffer應該剛建立出來就已經滿足被GC的條件,等下次GC執行的時候就應該可以被回收。
-
實際上卻沒這麼簡單。DirectByteBuffer是種典型的“冰山”物件,也就是說它的Java物件雖然很小很無辜,但它背後卻會關聯著一定量的native memory資源,而這些資源並不在GC的控制之下,需要自己注意控制好。
對JVM如何使用native memory不熟悉的同學可以研究一下這篇演講,“Where Does All the Native Memory Go”。
【盲點問題】DirectByteBuffer的回收問題
Oracle/Sun JDK的實現裡,DirectByteBuffer有幾處值得注意的地方。
- DirectByteBuffer沒有finalizer,它的native memory的清理工作是透過sun.misc.Cleaner自動完成的。
- sun.misc.Cleaner是一種基於PhantomReference的清理工具,比普通的finalizer輕量些。
"A cleaner tracks a referent object and encapsulates a thunk of arbitrary cleanup code. Some time after the GC detects that a cleaner's referent has become phantom-reachable, the reference-handler thread will run the cleaner."
原始碼註釋
/**
* General-purpose phantom-reference-based cleaners.
*
* <p> Cleaners are a lightweight and more robust alternative to finalization.
* They are lightweight because they are not created by the VM and thus do not
* require a JNI upcall to be created, and because their cleanup code is
* invoked directly by the reference-handler thread rather than by the
* finalizer thread. They are more robust because they use phantom references,
* the weakest type of reference object, thereby avoiding the nasty ordering
* problems inherent to finalization.
*
* <p> A cleaner tracks a referent object and encapsulates a thunk of arbitrary
* cleanup code. Some time after the GC detects that a cleaner's referent has
* become phantom-reachable, the reference-handler thread will run the cleaner.
* Cleaners may also be invoked directly; they are thread safe and ensure that
* they run their thunks at most once.
*
* <p> Cleaners are not a replacement for finalization. They should be used
* only when the cleanup code is extremely simple and straightforward.
* Nontrivial cleaners are inadvisable since they risk blocking the
* reference-handler thread and delaying further cleanup and finalization.
*
*
* @author Mark Reinhold
* @version %I%, %E%
*/
Oracle/Sun JDK中的HotSpot VM只會在Old Gen GC(Full GC/Major GC或者Concurrent GC都算)的時候才會對Old Gen中的物件做Reference Processing,而在Young GC/Minor GC時只會對Young Gen裡的物件做Reference processing。Full GC會對Old Gen做Reference processing,進而能觸發Cleaner對已死的DirectByteBuffer物件做清理工作。
-
如果很長一段時間裡沒做過GC或者只做了Young GC的話則不會在Old Gen觸發Cleaner的工作,那麼就可能讓本來已經死了的、但已經晉升到Old Gen的DirectByteBuffer關聯的Native Memory得不到及時釋放。
-
為DirectByteBuffer分配空間過程中會顯式呼叫System.gc(),以透過Full GC來強迫已經無用的DirectByteBuffer物件釋放掉它們關聯的native memory。
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size) {
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
if (size <= maxMemory - reservedMemory) {
reservedMemory += size;
return;
}
}
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (reservedMemory + size > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
}
}
總結分析
這幾個實現特徵使得Oracle/Sun JDK依賴於System.gc()觸發GC來保證DirectByteMemory的清理工作能及時完成。
如果開啟了-XX:+DisableExplicitGC,清理工作就可能得不到及時完成,於是就有機會見到direct memory的OOM,也就是上面的例子演示的情況。我們這邊在實際生產環境中確實遇到過這樣的問題。
如果你在使用Oracle/Sun JDK,應用裡有任何地方用了direct memory,那麼使用-XX:+DisableExplicitGC要小心。如果用了該引數而且遇到direct memory的OOM,可以嘗試去掉該引數看是否能避開這種OOM。如果擔心System.gc()呼叫造成Full GC頻繁,可以嘗試下面提到 -XX:+ExplicitGCInvokesConcurrent 引數