【JVM深層系列】JVM調優的「標準參數"的各種陷阱和坑點分析

java互聯搬磚工人 發佈 2022-12-17T16:16:09.211809+00:00

我參考了R大的分析和介紹,總結了一下相關的說明和分析結論。能觀察到的現象是:用一個案例來分析這現象:【盲點問題】DirectByteBuffer的回收問題。

易錯問題】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

  1. 當準備要觸發一次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);
  2. 如果有Perm/Meta gen的話,要在Perm/Meta gen分配空間但已經沒有足夠空間時,也要觸發一次full GC。
  3. 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調優建議里使用這個參數,對吧?但是為什麼要用它,什麼時候應該用而什麼時候用了會掉坑裡呢?

  1. 首先,要了解的是這個參數的作用。在Oracle/Sun JDK這個具體實現上,System.gc()的默認效果是引發一次stop-the-world的Full GC,由上面所知就是針對於整個GC堆做內存垃圾收集。
  2. 再次,如果採用了用了-XX:+DisableExplicitGC參數後,System.gc()的調用就會變成一個空調用,完全不會觸發任何GC(但是「函數調用」本身的開銷還是存在的哦~)。
  3. 為啥要用這個參數呢?最主要的原因是為了防止某些小白同學在代碼里到處寫System.gc()的調用而干擾了程序的正常運行吧。有些應用程式本來可能正常跑一天也不會出一次Full GC,但就是因為有人在代碼里調用了System.gc()而不得不間歇性被暫停。有些時候這些調用是在某些庫或框架里寫的,改不了它們的代碼但又不想被這些調用干擾也會用這參數。

-XX:+DisableExplicitGC看起來這參數應該總是開著嘛。有啥坑呢?

下述三個條件同時滿足時會發生的

  1. 應用本身在GC堆內的對象行為良好,正常情況下很久都不發生Full GC。
  2. 應用大量使用了NIO的direct memory,經常、反覆的申請DirectByteBuffer。
  3. 使用了-XX:+DisableExplicitGC。

能觀察到的現象是:

highlighter- code-theme-dark CSS

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)  

用一個案例來分析這現象:

java

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");  
  }  
}  

然後編譯、運行。

highlighter- code-theme-dark Bash

$ 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有幾處值得注意的地方。

  1. DirectByteBuffer沒有finalizer,它的native memory的清理工作是通過sun.misc.Cleaner自動完成的。
  2. 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."

源碼注釋

java

/** 
 * 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

java

// 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 參數

  • 【易錯問題】Major GC和Full GC的區別是什麼?觸發條件呢?
  • 在基於HotSpotVM的基礎角度
  • Partial GC(部分回收模式)
  • Full GC(全體回收模式)
  • 基於最簡單的分代式GC策略
  • 觸發條件是:Young GC
  • 觸發條件是:Full GC
  • 觸發條件是:Concurrent GC
  • GC回收器對應的GC模式列舉
  • GC回收模式的觸發總結
  • 【坑點與坑點】-XX:+DisableExplicitGC 與 NIO的direct memory的關係
  • -XX:+DisableExplicitGC看起來這參數應該總是開著嘛。有啥坑呢?
  • 下述三個條件同時滿足時會發生的
  • 能觀察到的現象是:
  • 用一個案例來分析這現象:
  • 【盲點問題】DirectByteBuffer的回收問題
  • 源碼注釋
  • 總結分析

本文來自博客園,作者:洛神灬殤,轉載請註明原文連結:https://www.cnblogs.com/liboware/p/16986641.html,任何足夠先進的科技,都與魔法無異。

關鍵字: