1. 程式人生 > 實用技巧 >JVM(七)——垃圾回收器

JVM(七)——垃圾回收器

iwehdio的部落格園:https://www.cnblogs.com/iwehdio/

1、垃圾回收的相關概念

  • 在預設情況下,通過System.gc()或者Runtime.getRuntime().gc()的呼叫,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件佔用的記憶體。

  • 然而System.gc()呼叫附帶一個免責宣告,無法保證對垃圾收集器的呼叫。實際上只是提醒JVM希望進行一次垃圾回收,但不確定是否會執行。

  • JVM實現者可以通過System.gc()呼叫來決定JVM的GC行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過於麻煩了。在一些特殊情況下,如我們正在編寫一個性能基準,我們可以在執行之間呼叫System.gc()

  • 實際上呼叫的是Runtime.getRuntime().gc()

  • 但是在System.gc()之後加上System.runFinalization(),則會強制呼叫失去引用的物件的finalize()方法。

  • 手動GC理解物件回收:

    public class Test2 {
    	//因為引用還在,不會被回收
        public void func1(){
            byte[] buffer = new byte[10*1024*1024];
            System.gc();
        }
        //引用消失,被回收
        public void func2(){
            byte[] buffer = new byte[10*1024*1024];
            buffer = null;
            System.gc();
        }
        //因為區域性變量表中的1的位置(0是this)還沒有被覆蓋,引用還在,不會被回收
        public void func3(){
            {
                byte[] buffer = new byte[10 * 1024 * 1024];
            }
            System.gc();
        }
        //區域性變量表中buffer被value覆蓋了,引用消失,被回收
        public void func4(){
            {
                byte[] buffer = new byte[10 * 1024 * 1024];
            }
            int value = 10;
            System.gc();
        }
        //方法出棧,引用消失,被回收
        public void func5(){
            func1();
            System.gc();
        }
        public static void main(String[] args) {
            Test2 test2 = new Test2();
            test2.func1();
        }
    }
    
  • 記憶體溢位(OOM):

    • javadoc中對OutOfMemoryError的解釋是,沒有空閒記憶體,並且垃圾收集器也無法提供更多記憶體。
    • JVM的堆記憶體不夠的主要原因:
      • JVM的堆記憶體設定不夠。
      • 程式碼中建立了大量大物件,並且長時間不能被垃圾回收器收集。
    • 在即將OOM時,JVM會進行一次獨佔式的Full GC操作,這時候會回收大量的記憶體,供應用程式繼續使用。
    • GC不是在任何情況下垃圾收驟器都會被觸發的。比如分配一個超大物件,超過堆的最大值,JVM可以判斷出垃圾收集並不能解決這個問題,所以直接丟擲OOM。
  • 記憶體洩漏:

    • 嚴格來講,只有物件不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體洩漏。
    • 但實際情況很多時候一些不太好的實踐(或疏忽)會導致物件的生命週期變得很長甚至導致OOM,也可以叫做寬泛意義上的記憶體洩漏。
    • 儘管記憶體洩漏並不會立刻引起程式崩潰,但是一旦發生記憶體洩漏,程式中的可用記憶體就會被逐步蠶食,直至耗盡所有記憶體,最終可能出現OutOfMemory異常,導致程式崩潰。
    • 這裡的儲存空間並不是指實體記憶體,而是指虛擬記憶體大小,這個虛擬記憶體大小取決於磁碟交換區設定的大小。
    • 記憶體洩露的例子:
      • 單例模式:單例的生命週期和應用程式是一樣長的,所以單例程式中,如果持有對外部物件的引用的話,那麼這個外部物件是不能被回收的,則會導致記憶體洩漏的產生。
      • 一些提供close的資源未關閉導致記憶體洩漏
        資料庫連線(dataSourse.getConnection()),網路連線(socket)和IO連線必須手動close,否則是不能被回收的。
  • Stop The World:

    • Stop-The-World,簡稱STW,指的是GC事件發生過程中,會產生應用程式的停頓。停頓產生時整個應用程式執行緒都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。
    • 可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。
      • 分析工作必須在一個能確保一致性的快照中進行。
      • 一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上。
      • 如果出現分析過程中物件引用關係還在不斷變化,則分析結果的準確性無法保證。
    • 被STW中斷的應用程式執行緒會在完成GC之後恢復,頻繁中斷會讓使用者感覺卡頓,所以我們需要減少STW的發生。
    • STW事件和採用哪款GC無關,所有的Gc都有這個事件。
    • STW是JVM在後臺自動發起和自動完成的。在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉。
    • 開發中不要用System.gc(),會導致stop-the-world的發生。
  • 垃圾回收的並行與併發:

    • 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態。如ParNew Parallel Scavenge,Parallel Old。
    • 序列(Serial):相較於並行的概念,單執行緒執行。
      如果記憶體不夠,則程式暫停,啟動JVM垃圾回收器進行垃圾回收。回收完,再啟動程式的執行緒(實際上並行也是如此)。
    • 併發(Concurrent):在一個時間段內,指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓使用者程式的執行。使用者程式在繼續執行,而垃圾收集程式執行緒運行於另一個CPU上;如CMS,G1。
  • 安全點和安全區域:

    • 安全點:程式執行時並非在所有地方都能停頓下來開始Gc,只有在特定的位置才能停頓下來開始GC,這些位置稱為“安全點(Safepoint),
    • Safe Point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致執行時的效能問題。
    • 大部分指令的執行時間都非常短暫,通常會根據“是否具有讓程式長時間執行的特徵”為標準。比如:選擇一些執行時間較長的指令作為Safe Point,如方法呼叫、迴圈跳轉和異常跳轉等。
    • 如何在GC發生時,檢查所有執行緒都跑到最近的安全點停頓下來呢?
      • 搶先式中斷:(目前沒有虛擬機器採用了)。首先中斷所有執行緒。如果還有執行緒不在安全點,就恢復執行緒,讓執行緒跑到安全點。
      • 主動式中斷:設定一箇中斷標誌,各個執行緒執行到Safe Point的時候主動輪詢這個標誌,如果中斷標誌為真,則將自己進行中斷掛起。
    • Safepoint機制保證了程式執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程式“不執行”的時候,例如執行緒處於Sleep狀態或Blocked狀態,這時候執行緒無法響應JVM的中斷請求,“走”到安全點去中斷掛起,JVM也不太可能等待執行緒被喚醒。對於這種情況,就需要安全區域(Safe Region)來解決。
    • 安全區域是指在一段程式碼片段中,物件的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。我們也可以把safe Region看做是被擴充套件了的Safepoint。
    • 執行緒在進 Safe Region的時候先標記自己已進入了Safe Region,這樣GC時就不需要管進入Safe Region的執行緒了。等到被喚醒時準備離開Safe Region時,先檢查能否離開,如果GC完成了,那麼執行緒可以離開,否則它必須等待直到收到安全離開的訊號為止。
  • 垃圾回收與物件引用:

    • 我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體中;如果記憶體空間在進行垃圾收集後還是很緊張,則可以拋棄這些物件。
    • 在JDK 1.2版之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用
      (Phantom Reference)4種,這4種引用強度依次逐漸減弱。
    • 強引用(StrongReference):最傳統的“引用”的定義,是指在程式程式碼之中普遍有在的引用賦值,即類似"object obj=new object()"這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。
    • 軟引用(SoftReference):在系統將要發生記憶體溢位之前,將會把這些物件列入回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
    • 弱引用(WeakReference):被弱引用關聯的物件只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收掉被弱引用關聯的物件。
    • 虛引用(PhantomReference):一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個物件的例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
    • 強引用:永不回收;軟引用:記憶體不夠即回收;弱引用:被垃圾回收發現即回收;虛引用:作為物件回收跟蹤。
  • 強引用:

    • 在Java程式中,最常見的引用型別是強引用(普通系統99%以上都是強引用),也就是我們最常見的普通物件引用,也是預設的引用型別。
    • 當在Java語言中使用new操作符建立一個新的物件,並將其賦值給一個變數的時候,這個變數就成為指向該物件的一個強引用。
    • 強引用的物件是可觸及的,垃圾收集器就永遠不會回收掉被引用的物件。
    • 對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。
    • 相對的,軟引用、弱引用和虛引用的物件是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的。所以,強引用是造成Java記憶體洩漏的主要原因之一。
    • 強引用具備以下特點:
      • 強引用可以直接訪問目標物件。
      • 強引用所指向的物件在任何時候都不會被系統回收,虛擬機器寧願丟擲OOM異常,也不會回收強引用所指向物件。
      • 強引用可能導致記憶體洩漏。
  • 軟引用:

    • 軟引用是用來描述一些還有用,但非必需的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收(第一次回收是針對不可達物件),如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。

    • 軟引用通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟引用。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。

    • 垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟引用,並可選地把引用存放到一個引用佇列(Reference Queue)。

    • 類似弱引用,只不過Java虛擬機器會盡量讓軟引用的存活時間長一些,迫不得已才清理。

    • 實現軟引用:

      Object obj = new object();	//宣告強引用
      SoftReferencecobjet f= new SoftReference<object>(obj);	//生成軟引用
      obj = null;	//銷燬強引用
      Object o = f.get();	//用軟引用獲取物件
      
  • 弱引用:

    • 弱引用也是用來描述那些非必需物件,只被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。在系統Gc時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的物件。

    • 但是,由於垃圾回收器的執行緒通常優先順序很低,因此,並不一定能很快地發現持有弱引用的物件。在這種情況下,弱引用物件可以存在較長的時間。

    • 弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用佇列,當弱引用物件被回收時,就會加入指定的引用佇列,通過這個佇列可以跟蹤物件的回收情況。

    • 軟引用、弱引用都非常適合來儲存那些可有可無的快取資料。如果這麼做,當系統記憶體不足時,這些快取資料會被回收,不會導致記憶體溢位。而當記憶體資源充足時,這些快取資料又可以存在相當長的時間,從而起到加速系統的作用。

    • 弱引用物件與軟引用物件的最大不同就在於,當GC在進行回收時,需要通過演算法檢查是否回收軟引用物件,而對於弱引用物件,GC總是進行回收。弱引用物件更容易、更快被GC回收。

    • 實現弱引用:

      WeakReferencecobjet f= new WeakReference<object>(new Object());	//生成弱引用
      Object o = f.get();	//用弱引用獲取物件
      
  • 虛引用:

    • 是所有引用型別中最弱的一個。

    • 一個物件是否有虛引用的存在,完全不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。

    • 它不能單獨使用,也無法通過虛引用來獲取被引用的物件。當試圖通過虛引用的get()方法取得物件時,總是null。

    • 為一個物件設定虛引用關聯的唯一目的在於跟蹤垃圾回收過程。比如:能在這個物件被收集器回收時收到一個系統通知。

    • 虛引用必須和引用佇列一起使用。虛引用在建立時必須提供一個引用佇列作為引數。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件後,將這個虛引用加入引用佇列,以通知應用程式物件的回收情況。

    • 由於虛引用可以跟蹤物件的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。

    • 實現虛引用:

      object obj = new object(); 
      ReferenceQueue phantomQueue = new Referencequeue(); 
      PhantomReferencekobject> pf = new PhantomReferencecobject>(obj, phantomQueue); obj = null;
      
  • 終結器引用:

    • 它用以實現物件的finalize()方法,也可以稱為終結器引用。
    • 無需手動編碼,其內部配合引用佇列使用。
    • 在GC時,終結器引用入隊。由Finalizer執行緒通過終結器引用找到被引用物件並呼叫它的finalize()方法,第二次GC時才能回收被引用物件。

2、垃圾回收器

  • 垃圾回收器的分類:

    • 按執行緒數分:
      • 序列回收指的是在同一時間段內只允許有一個CPU用於執行垃圾回收操作,此時工作執行緒被暫停,直至垃圾收集工作結束。
        • 在諸如單CPU處理器或者較小的應用記憶體等硬體平臺不是特別優越的場合,序列回收器的效能表現可以超過並行回收器和併發回收器。所以,序列回收預設被應用在客戶端的Client模式下的JVM中
        • 在併發能力比較強的CPU上,並行回收器產生的停頓時聞要短於序列回收器。
      • 並行收集可以運用多個CPU同時執行垃圾回收,因此提升了應用的吞吐量,不過並行回收仍然與序列回收一樣,採用獨佔式,使用了"stop-the-world"機制。
    • 按照工作模式分:
      • 併發式垃圾回收器與應用程式執行緒交替工作,以儘可能減少應用程式的停頓時間。
      • 獨佔式垃圾回收器(stop the world)一旦執行,就停止應用程式中的所有使用者執行緒,直到垃圾回收過程完全結束。
    • 按碎片處理方式分:
      • 壓縮式垃圾回收器會在回收完成後,對存活物件進行壓縮整理,消除回收後的碎片。
      • 非壓縮式的垃圾回收器不進行這步操作。
    • 按工作的記憶體區間分,又可分為年輕代垃圾回收器和老年代垃圾回收器。
  • 評估垃圾回收器的指標:

    • 吞吐量:執行使用者程式碼的時間佔總執行時間的比例
      (總執行時間:程式的執行時間+記憶體回收的時間)。
    • 垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總執行時間的比例。
    • 暫停時間:執行垃圾收集時,程式的工作執行緒被暫停的時間。
    • 收集頻率:相對於應用程式的執行,收集操作發生的頻率。
    • 記憶體佔用:Java堆區所佔的記憶體大小。
    • 快速:一個物件從誕生到被回收所經歷的時間。
    • 最重要的是吞吐量和暫停時間。
    • 高吞吐量較好因為這會讓應用程式的終端使用者感覺有應用程式執行緒在做“生產性”工作。直覺上,吞吐量越高程式執行越快。
    • 低暫停時間(低延遲)較好因為從終端使用者的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。因此,具有低的較大暫停時間是非常重要的,特別是對於一個互動式應用程式。
    • 在設計(或使用)GC演算法時,我們必須確定我們的目標:一個GC演算法只可能針對兩個目標之一(即只專注於較大吞吐量或最小暫停時間),或嘗試找到一個二者的折衷。
  • 七種經典的垃圾回收器:

    • 序列回收器:Serial,Serial old。
    • 並行回收器:ParNew,Parallel Scavenge,Parallel old。
    • 併發回收器:CMS,G1。
    • 為什麼要有很多收集器?因為Java的使用場景很多,移動端,伺服器等。所以就需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的效能。我們選擇的只是對具體應用最合適的收集器。
  • 垃圾回收器與垃圾分代:

    • 新生代收集器:Serial,ParNew,Parallel scavenge;
    • 老年代收集器:Serial old,Parallel old,CMS;
    • 整堆收集器:G1。

  • 垃圾回收器的組合關係:

    • jdk8之前,所有的線(實線和虛線)之間都可以組合。
    • 在jdk8中,紅色虛線被廢棄;在jdk9中被移除。
    • 在jdk14中,綠色虛線被廢棄,青色虛線(CMS)被刪除。
    • Serial old與CMS的連線是表示,其作為老年代CMS收集器的後備垃圾收集方案。
  • 檢視使用的垃圾回收器:

    • -XX:+PrintCommandLineFlags:檢視命令列相關引數(包含使用的垃圾收集器)
    • 使用命令列指令:jinfo -flag 相關垃圾回收引數 程序ID。
    • jdk6~8預設Parallel scavenge + Parallel Old。
    • jdk9及以後預設G1。
  • Serial回收器:序列回收

    • Serial收集器是最基本、歷史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的選擇。
    • Serial收集器作為Hotspot中Client模式下的預設新生代垃圾收集器。
    • Serial 收集器採用複製演算法、序列回收和"Stop-the-World"機制的方式執行記憶體回收。|
    • 除了年輕代之外,Serial收集器還提供用於執行老年代垃圾收集的Serial old收集器。Serial old收集器同樣也採用了序列回收和"Stop the World"機制,只不過記憶體回收演算法使用的是標記-壓縮演算法。
      • Serial old是執行在Client模式下預設的老年代的垃圾回收器
      • Serial old在Server模式下主要有兩個用途:①與新生代的Parallel Scavenge配合使用;②作為老年代CMS收集器的後備垃圾收集方案。
    • 這個收集器是一個單執行緒的收集器,但它的“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束(stop The world)。
    • 優勢:簡單而高效(與其他收集器的單執行緒比),對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。
      • 執行在Client模式下的虛擬機器是個不錯的選擇。
    • 在使用者的桌面應用場景中,可用記憶體一般不大(幾十MB至一兩百MB),可以在較短時間內完成垃圾收集(幾十ms至一百多ms),只要不頻繁發生,使用序列回收器是可以接受的。
    • 在Hotspot虛擬機器中,使用-XX:+UseSerialGC引數可以指定年輕代和老年代都使用序列收集器。等價於新生代用Serial GC,且老年代用Serial old GC。
    • 現在已經不用序列的了。而且在限定單核cpu才可以用。
    • 對於互動較強的應用而言,這種垃圾收集器是不能接受的。一般在Java web應用程式中是不會採用序列垃圾收集器的。

  • ParNew回收器:並行回收

    • 如果說Serial GC是年輕代中的單執行緒垃圾收集器,那麼ParNew收集器則是Serial收集器的多執行緒版本。
      • Par是Parallel的縮寫,New:只能處理的是新生代。
    • ParNew收集器除了採用並行回收的方式執行記憶體回收外,兩款垃圾收集器之間幾乎沒有任何區別。
    • ParNew收集器在年輕代中同樣也是採用複製演算法、"stop-the-world"機制。
    • ParNew 是很多JVM執行在Server模式下新生代的預設垃圾收集器。

    • 對於新生代,回收次數頻繁,使用並行方式高效。
    • 對於老年代,回收次數少,使用序列方式節省資源。(CPU並行需要切換執行緒,序列可以省去切換執行緒的資源)
    • 由於ParNew收集器是基於並行回收,那麼是否可以斷定ParNew收集器的回收效率在任何場景下都會比Serial收集器更高效?
      • ParNew收集器執行在多CPU的環境下,由於可以充分利用多CPU、多核心等物理硬體資源優勢,可以更快速地完成垃圾收集,提升程式的吞吐量。
      • 但是在單個CPU的環境下,ParNew收集器不比Serial收集器更高效。雖然Serial收集器是基於序列回收,但是由於CPU不需要頻繁地做任務切換,因此可以有效避免多執行緒互動過程中產生的一些額外開銷。
    • 除Serial外,只有ParNew GC能與CMS收集器配合工作。
    • 在程式中,開發人員可以通過選項”-XX:+UseParNewGC"手動指定使用ParNew收集器執行記憶體回收任務。它表示年輕代使用並行收集器,不影響老年代。
    • -XX:ParallelGCThreads限制執行緒數量,默以開啟和CPU資料相同的執行緒數。
  • Parallel Scavenge回收器:吞吐量優先

    • HotSpot的年輕代中除了擁有ParNew收集器是基於並行回收的以外,Parallel Scavenge收集器同樣也採用了複製演算法、並行回收和"Stop the World"機制。
    • 那麼Parallel收集器的出現是否多此一舉?
      • 和ParNew收集器不同,Parallel scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput),它也被稱為吞吐量優先的垃圾收集器。
      • 自適應調節策略也是Parallel Scavenge與ParNew一個重要區別。
    • 高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。因此,常見在伺服器環境中使用。
    • Parallel收集器在JDK1.6時提供了用於執行老年代垃圾收集的Parallel old收集器,用來代替老年代的Serial old收集器。
    • Parallel old收集器採用了標記-壓縮演算法,但同樣也是基於並行回收和"stop-the-world"機制。
    • 在程式吞吐量優先的應用場景中,Parallel收集器和Parallel old收集器的組合,在server模式下的記憶體回收效能很不錯。
    • 在jdk8中,預設是Parallel+Parallel old垃圾收集器。

    • -XX:+UseParallelGC手動指定年輕代使用Paralle1並行收集器執行記憶體回收任務。
    • -XX:+UseParalleloldGC 手動指定老年代都是使用並行回收收集器。
    • 分別適用於新生代和老年代。預設dk8是開啟的。
      上面兩個引數,預設開啟一個,另一個也會被開啟。(互相啟用)
    • -XX:ParallelGCThreads 設定年輕代並行收集器的執行緒數。
      • 一般地,最好與CPU數量相等,以避免過多的執行緒數影響垃圾收集效能。
      • 在預設情況下,當CPU數量小於等於8個,ParallelGCThreads的值等於CPU數量。
      • 當CPU數量大於8個,ParallelGCThreads的值等於3+[(5*CPU Count)/8]。
    • -XX:MaxGcPauseMillis 設定垃圾收集器最大停頓時間(即STw的時間)。單位是毫秒。
      • 為了儘可能地把停頓時間控制在MaxGCPauseMills以內,收集器在工作時會調整Java堆大小或者其他一些引數。
      • 對於使用者來講,停頓時間越短體驗越好。但是在伺服器端,我們注重高併發,整體的吞吐量。所以伺服器端適合Parallel,進行控制。
      • 該引數使用需謹慎。
    • -XX:GCTimeRatio 垃圾收集時間佔總時間的比例(=1/(N+1))。
      • 用於衡量吞吐量的大小。
      • 取值範圍(0,100)。預設值99,也就是垃圾回收時間不超過1%。
      • 與前一個-XX:MaxGCPauseMillis引數有一定矛盾性。暫停時間越長,Radio引數就容易超過設定的比例。
    • -XX:+UseAdaptivesizePolicy 設定Parallel Scavenge收集器具有自適應調節策略
      • 在這種模式下,年輕代的大小、Eden和Survivor的比例、晉升老年代的物件年齡等引數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。
      • 在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機器的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMills),讓虛擬機器自己完成調優工作。
  • CMS回收器:低延遲

    • 在JDK1.5時期,HotSpot 推出了一款在強互動應用中幾乎可認為有劃時代意義的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,這款收集器是HotSpot虛擬機器中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒同時工作。
    • CMS收集器的關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間。停頓時間越短(低延遲)就越適合與使用者互動的程式,良好的響應速度能提升使用者體驗。
      • 目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。
      • CMS收集器就非常符合這類應用的需求。
    • CMS的垃圾收集演算法採用標記-清除演算法,並且也會"stop-the-world"。
    • CMS作為老年代的收集器,無法與JDK1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。

    • CMS整個過程比之前的收集器要複雜,整個過程分為4個主要階段,即初始標記階段、併發標記階段、重新標記階段和併發清除階段。
      • 初始標記(Initial-Mark)階段:在這個階段中,程式中所有的工作執行緒都將會因為
        “stop-the-World”機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記出GCRoots能直接關聯到的物件。一旦標記完成之後就會恢復之前被暫停的所有應用執行緒。由於直接關聯物件比較小,所以這裡的速度非常快。
      • 併發標記(Concurrent-Mark)階段:從GC Roots的直接關聯物件開始遍歷整個物件圖的過程,這個過程耗時較長但是不需要停頓使用者執行緒,可以與垃圾收集執行緒一起併發執行。
      • 重新標記(Remark)階段:由於在併發標記階段中,程式的工作執行緒會和垃圾收集執行緒同時執行或者交叉執行,因此為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。
      • 併發清除(Concurrent-Sweep)階段:此階段清理刪除掉標記階段判斷的已經死亡的物件,釋放記憶體空間。由於不需要移動存活物件,所以這個階段也是可以與使用者執行緒同時併發的。
    • 儘管CMS收集器採用的是併發回收(非獨佔式),但是在其初始化標記和再次標記這兩個階段中仍然需要執行“stop-the-World”機制暫停程式中的工作執行緒,不過暫停時間並不會太長,因此可以說明目前所有的垃圾收集器都做不到完全不需要“stop-the-
      World”,只是儘可能地縮短暫停時間。
    • 由於最耗費時間的併發標記與併發清除階段都不需要暫停工作,所以整體的回收是低停頓的。
    • 另外,由於在垃圾收集階段使用者執行緒沒有中斷,所以在CMS回收過程中,還應該確保應用程式使用者執行緒有足夠的記憶體可用。因此,CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當堆記憶體使用率達到某一閾值時,便開始進行回收,以確保應用程式在CMS工作過程中依然有足夠的空間支援應用程式執行。
    • 要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。
    • CMS收集器的垃圾收集演算法採用的是標記一清除演算法,這意味著每次執行完記憶體回收後,由於被執行記憶體回收的無用物件所佔用的記憶體空間極有可能是不連續的一些記憶體塊,不可避免地將會產生一些記憶體碎片。那麼CMS在為新物件分配記憶體空間時,將無法使用指標碰撞(Bump the Pointer)技術,而只能夠選擇空閒列表(Free List)執行記憶體分配。
    • 既然Mark Sweep會造成記憶體碎片,那麼為什麼不把演算法換成Mark Compact呢?
      • 因為當併發清除的時候,用Compact整理記憶體的話,原來的使用者執行緒使用的記憶體就無法使用。要保證使用者執行緒能繼續執行,前提的它執行的資源不受影響。Mark Compact更適合“stop the world”這種場景下使用。
    • 優點:
      • 併發收集。
      • 低延遲。
    • 缺點:
      • 會產生記憶體碎片,導致併發清除後,使用者執行緒可用的空間不足。在無法分配大物件的情況下,不得不提前觸發Full GC。
      • CMS收集器對cpu資源非常敏感。在併發階段,它雖然不會導致使用者停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低。
      • CMS收集器無法處理浮動垃圾。可能出現“Concurrent Mode Failure"失敗而導致另一次Full GC的產生。在併發標記階段由於程式的工作執行緒和垃圾收集執行緒是同時執行或者交叉執行的,那麼在併發標記階段如果產生新的垃圾物件,CMS將無法對這些垃圾物件進行標記,最終會導致這些新產生的垃圾物件沒有被及時回收,從而只能在下一次執行GC時釋放這些之前未被回收的記憶體空間。
    • CMS可以設定的引數:
      • -XX:+UseConcMarkSweepGC手動指定使用CMS收集器執行記憶體回收任務。
        • 開啟該引數後會自動將-XX:+UseParNewGc開啟。即:ParNew(Young區用)+CMS(old區用)+Serial old的組合。
      • -XX:CMSlnitiatingoccupanyFraction 設定堆記憶體使用率的閾值,一旦達到該閾值,便開始進行回收。
        • JDK5及以前版本的預設值為68,即當老年代的空間使用率達到68%時,會執行一次cMs回收。JDK6及以上版本預設值為92%。
        • 如果記憶體增長緩慢,則可以設定一個稍大的值,大的閥值可以有效降低CMS的觸發頻率,減少老年代回收的次數可以較為明顯地改善應用程式效能。反之,如果應用程式記憶體使用率增長很快,則應該降低這個閥值,以避免頻繁觸發老年代序列收集器。因此通過該選項便可以有效降低Full GC的執行次數。
      • -XX:+UseCMSCqmpactAtFullCollection 用於指定在執行完Full GC後對記憶體空間遞行壓縮整理,以此避免記憶體碎片的產生。不過由於記憶體壓縮整理過程無法併發執行,所帶來的問題就是停頓時間變得更長了。
      • -XX:CMSFullGCsBeforecompaction 設定在執行多少次Ful1GC後對記憶體空間進行壓縮整理。
      • -XX:ParallelCMSThreads 設定cMs的執行緒數量。
        • CMS預設啟動的執行緒數是(ParallelGCThreads+3)/4,ParallelGCThreads 是年輕代並行收集器的執行緒數。
        • 當CPU資源比較緊張時,受到CMS收集器執行緒的影響,應用程式的效能在垃圾回收階段可能會非常糟糕。
    • GC的選擇:
      • 如果你想要最小化地使用記憶體和並行開銷,請選Serial GC;
      • 如果你想要最大化應用程式的吞吐量,請選Parallel GC;
      • 如果你想要最小化GC的中斷或停頓時間,請選CMS GC。
  • G1回收器:區域化分代式。

    • 應用程式所應對的業務越來愈龐大,經常造成STW的GC又跟不上實際的需求。官方給G1設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,擔當起“全功能收集器”的期望。

    • 為什麼名字叫做Garbage First(G1)?。

      • 因為G1是一個並行回收器,它把堆記憶體分割為很多不相關的區域(Region)(物理上不連續的)。使用不同的Region來表示Eden、倖存者0區,倖存者1區,老年代等。
      • G1 GC有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。
      • 由於這種方式的側重點在於回收垃圾最大量的區間(Region),所以我們給G1一個名字:垃圾優先(Garbage First)。
    • 優點:

      • 並行與併發:
        • 並行性:G1在回收期間,可以有多個GC執行緒同時工作,有效利用多核計算能力。此時使用者執行緒STW。
        • 併發性:G1擁有與應用程式交替執行的能力,部分工作可以和應用程式同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程式的情況。
      • 分代收集:
        • 從分代上看,G1依然屬於分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和Survivor區。但從堆的結構上看,它不要求整個Eden區、年輕代或者老年代都是連續的,也不再堅持固定大小和固定數量。
        • 將堆空間分為若干個區域(Region),這些區域中包含了邏輯上的年輕代和老年代。
        • 和之前的各類回收器不同,它同時兼顧年輕代和老年代。其他回收器,或工作在年輕代,或工作在老年代。
      • 空間整合:
        • CMS:“標記-清除”演算法、記憶體碎片、若干次GC後進行一次碎片整理。
        • G1將記憶體劃分為一個個的region。記憶體的回收是以region作為基本單位的。
          • Region之間是複製演算法,但整體上實際可看作是標記-壓縮(Mark-Compact)
            演算法,兩種演算法都可以避免記憶體碎片。
          • 這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。尤其是當Java堆非常大的時候,G1的優勢更加明顯。
      • 可預測的停頓時間模型(即:軟實時soft real-time):
        • 這是G1相對於CMS的另一大優勢,G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
        • 由於分割槽的原因,G1可以只選取部分割槽域進行記憶體回收,這樣縮小了回收的範圍,因此對於全域性停頓情況的發生也能得到較好的控制。
        • G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。
        • 相比於CMSGC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。
    • 缺點:

      • 相較於CMS,G1還不具備全方位、壓倒性優勢。比如在使用者程式執行過程中,G1無論是為了垃圾收集產生的記憶體佔用(Footprint)還是程式執行時的額外執行負載(Overload)都要比CMS要高。
      • 從經驗上來說,在小記憶體應用上CMS的表現大概率會優於G1,而G1在大記憶體應用上則發揮其優勢。平衡點在6-8GB之間。
    • G1可以設定的引數:

      • -XX:+UseG1GC手動指定使用G1收集器執行記憶體回收任務。
      • -XX:G1HeapRegipnsize設定每個Region的大小。值是2的冪,範圍是1MB到32MB之間,目標是根據最小的Java堆大小劃分出約2048個區域。預設是堆記憶體的1/2000。
      • -XX:MaxGCPauseMi1lis 設定期望達到的最大Gc停頓時間指標(JVM會盡力實現,但不保證達到)。預設值是200ms。
      • -XX:ParallelGCThread 設定STw工作執行緒數的值。最多設定為8
      • -XX:ConcGCThreads設定併發標記的執行緒數。將n設定為並行垃圾回收執行緒(ParallelGcThreads)的1/4左右。
      • -XX:InitiatingHeapoccupancyPercent 設定觸發併發GC週期的Java堆佔用率閥值。超過此值,就觸發GC。預設值是45。
    • G1的使用操作:

      • 第一步:開啟61垃圾收集器;
      • 第二步:設定堆的最大記憶體;
      • 第三步:設定最大的停頓時間;
      • G1中提供了三種垃圾回收模式:YoungGC、Mixed Gc和Fu11GC,在不同的條件下被觸發。
    • G1的適用場景:

      • 面向服務端應用,針對具有大記憶體、多處理器的機器。在普通大小的堆裡表現並不驚喜)。

      • 最主要的應用是需要低GC延遲,並具有大堆的應用程式提供解決方案;如:在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒;(G1通過每次只清理一部分而不是全部的Region的增量式清理來保證每次Gc停頓時間不會過長)。

      • 用來替換掉JDK1.5中的CMS收集器;在下面的情況時,使用G1可能比CMS好:

        ①超過50%的Java堆被活動資料佔用;
        ②物件分配頻率或年代提升頻率變化很大;
        ③GC停頓時間過長(長於0.5至1秒)。

        HotSpot垃圾收集器裡,除了G1以外,其他的垃圾收集器使用內建的JVM執行緒執行GC的多執行緒操作,而61GC可以採用應用執行緒承擔後臺執行的GC工作,即當JVM的GC執行緒處理速度慢時,系統會呼叫應用程式執行緒幫助加速垃圾回收過程。

    • 分割槽Region:化整為零。

      • 使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據堆空間的實際大小而定,整體被控制在1MB到32MB之間,且為2的N次冪。所有的Region大小相同,且在JVw生命週期內不會被改變。

      • 雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。通過Region的動態分配方式實現邏輯上的連續。

      • 一個region 有可能屬於Eden,Survivor 或者Old/Tenured記憶體區域。但是一個region只可能屬於一個角色。E表示該region屬於Eden記憶體區域,S表示屬於survivor記憶體區域,O表示屬於old記憶體區域。空白的表示未使用的記憶體空間。

      • G1垃圾收集器還增加了一種新的記憶體區域,叫做Humongous記憶體區域,H塊。主要用於儲存大物件,如果超過0.5個region,就放到H。

      • 設定H的原因:對於堆中的大物件,預設直接會被分配到老年代,但是如果它是一個短期存在的大物件,就會對垃圾收集器造成負面影響。為了解決這個閘題,G1劃分了一個Humongous區,它用來專門存放大物件。如果一個H區裝不下一個大物件,那麼G1會尋找連續的H區來儲存。為了能找到連續的H區,有時候不得不啟動Full GC。G1的大多數行為都把H區作為老年代的一部分來看待。

    • G1垃圾回收過程:主要包括三個環節。

      • 年輕代GC(Young GC)。

        • 應用程式分配記憶體,當年輕代的Eden區用盡時開始年輕代回收過程;G1的年輕代收集階段是一個並行的獨佔式收集器。
        • 在年輕代回收期,G1GC暫停所有應用程式執行緒(STW),啟動多執行緒執行年輕代回收。然後從年輕代區間移動存活物件到survivor區間或者老年區間,也有可能是兩個區間都會涉及。

      • 老年代併發標記過程(Concurrent Marking)。

        • 當堆記憶體使用達到一定值(預設45%)時,開始老年代併發標記過程。

      • 混合回收(Mixed GC)。

        • 標記完成馬上開始混合回收過程。對於一個混合回收期,G1GC從老年區間移動存活物件到空閒區間,這些空閒區間也就成為了老年代的一部分。
        • 和年輕代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收一小部分老年代的Region就可以了。同時,這個老年代Region是和年輕代一起被回收的。

      • (如果需要,單執行緒、獨佔式、高強度的FullGC還是繼續存在的。它針對GC的評估失敗提供了一種失敗保護機制,即強力回收)。

    • Remembered Set:記憶集RSet。

      • 一個Region不可能是孤立的,一個Region中的物件可能被其他任意Region中物件引用,判斷物件存活時,需要掃描整個Java堆才能保證準確。

      • 在其他的分代收集器,也存在這樣的問題(而G1更突出)回收新生代也不得不同時掃描老年代,這樣的話會降低MinorGC的效率。

      • 無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全域性掃描:
        每個Region都有一個對應的Remembered Set。

      • 每次Reference型別資料寫操作時,都會產生一個Write Barrier(寫屏障)暫時中斷操作;然後檢查將要寫入的引用指向的物件是否和該Reference型別資料在不同的Region(對於除G1外的其他收集器:檢查老年代物件是否引用了新生代物件);

      • 如果不同,通過CardTable把相關引用資訊記錄到引用指向物件的所在Region對應的Remembered Set中;當進行垃圾收集時,在GC根節點的列舉範圍加入Remembered Set;就可以保證不進行全域性掃描,也不會有遺漏。

    • G1優化建議:

      • 年輕代大小:
        • 避免使用-xmn或-XX:NewRatio等相關選項顯式設定年輕代大小。
        • 固定年輕代的大小會覆蓋暫停時間目標。
      • 暫停時間目標不要太過嚴苛:
        • G1GC的吞吐量目標是90%的應用程式時間和10%的垃圾回收時間
        • 評估G1GC的吞吐量時,暫停時間目標不要太嚴苛。目標太過嚴苛表示你願意承受更多的垃圾回收開銷(次數),而這些會直接影響到吞吐量。
  • 垃圾回收器總結:

    • 7種經典垃圾回收器:

    • GC發展階段:Serial =>Parallel(並行)=>CMS(併發)=>G1=>ZGC。

    • 怎麼選擇垃圾收集器?

      1.優先調整堆的大小讓JVM自適應完成。

      2.如果記憶體小於100M,使用序列收集器。

      3.如果是單核、單機程式,並且沒有停頓時間的要求,序列收集器。

      4.如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇並行或者JVM自己選擇。
      5.如果是多CPU、追求低停頓時間,需快速響應(比如延遲不能超過1秒,如網際網路應用),使用併發收集器。
      官方推薦G1,效能高。現在網際網路的專案,基本都是使用G1。

  • ZGC:革命性的。

    • ZGC在儘可能對吞吐量影響不大的前提下,實現在任意堆記憶體大小下都可以把垃圾收集的停頓時間限制在十毫秒以內的低延遲。
    • ZGC收集器是一款基於Region記憶體佈局的,(暫時)不設分代的,使用了讀屏障、染色指標和記憶體多重對映等技術來實現可併發的標記-壓縮演算法的,以低延遲為首要目標的一款垃圾收集器。
    • ZGC的工作過程可以分為4個階段:併發標記-併發預備重分配-併發重分配-併發重對映等。
    • ZGC幾乎在所有地方併發執行的,除了初始標記的是STW的。所以停頓時間幾乎就耗費在初始標記上,這部分的實際時間是非常少的。

3、GC日誌

  • JVM記憶體分配與垃圾回收日誌:

    • -XX:+PrintGC 輸出GC日誌。類似:-verbose:gc。
    • -XX:+PrintGCDetails 輸出GC的詳細日誌。
    • -XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)。
    • -XX:+PrintGCDatestamps 輸出GC的時間戳(以日期的形式,如2013-05-04T21:53:59.234+0800)。
    • -XX:+PrintHeapAtGC在進行Gc的前後打印出堆的資訊。
    • -Xloggc:../1ogs/gc.1og 日誌檔案的輸出路徑。
  • 日誌補充說明:

    • "[GC"和”[FullGC"說明了這次垃圾收集的停頓型別,如果有"Full”則說明GC發生了"stop The World"。

    • 使用Seria1收集器在新生代的名字是Default New Generation,因此顯示的是"[DefNew"。

    • 使用ParNew收集器在新生代的名字會變成”[ParNew”,意思是"Parallel New Generation"。

    • 使用Parallel Scavenge收集器在新生代的名字是"[PSYoungGen"。

    • 老年代的收集和新生代道理一樣,名字也是收集器決定的

    • 使用G1收集器的話,會顯示為"garbage-first heap"。

    • Allocation Failure表明本次引起GC的原因是因為在年輕代中沒有足夠的空間能夠儲存新的資料了。

    • [PSYoungGen:5986K->696K(8704K)]5986K->704K(9216K)
      中括號內:GC回收前年輕代大小,回收後大小,(年輕代總大小)。

      括號外:GC回收前年輕代和老年代大小,回收後大小,(年輕代和老年代總大小)。

    • user代表使用者態回收耗時,sys核心態回收耗時,real實際耗時。由於多核的原因,時間總和可能會超過real時間。

  • Young GC日誌:

  • Full GC日誌:

  • 從GC日誌看不同GC的不同行為:

    • 執行如下程式碼:

      public class GCLog{
      	private static final int _1MB=1024*1024;
          public static void testAllocation(){
              byte[] allocation1,allocation2,allocation3,allocation4;
              allocation1=new byte[2*_1MB];
              allocation2=new byte[2*_1MB];
              allocation3=new byte[2*_1MB];
              allocation4=new byte[4*_1MB];
          }
          public static void main(String[] agrs){
              testAllocation();
          }
      }
      
    • JVM記憶體設定,Eden區8M,S0/S1區1M。

    • 使用Serial GC(JDK8中需指定),先存入三個2M的,當第四個4M的申請記憶體時,先進行Young GC,發現2M的大小都無法放入S0區,則將前三個直接放入老年代,4M的放入Eden區。

    • 使用Parallel Scavenge GC,先存入三個2M的,當第四個4M的申請記憶體時,不會發生Young GC,直接將4M的存入老年代。


iwehdio的部落格園:https://www.cnblogs.com/iwehdio/