【JVM】JVM系列之垃圾回收(二)

1、爲何須要垃圾回收html

  若是不進行垃圾回收,內存早晚都會被消耗空,由於咱們在不斷的分配內存空間而不進行回收。除非內存無限大,咱們能夠任性的分配而不回收,可是事實並不是如此。因此,垃圾回收是必須的。java

2、哪些內存須要進行垃圾回收算法

  對於虛擬機中線程私有的區域,如程序計數器、虛擬機棧、本地方法棧都不須要進行垃圾回收,由於它們是自動進行的,隨着線程的消亡而消亡,不須要咱們去回收,好比棧的棧幀結構,當進入一個方法時,就會產生一個棧幀,棧幀大小也能夠藉助類信息肯定,而後棧幀入棧,執行方法體,退出方法時,棧幀出棧,因而其所佔據的內存空間也就被自動回收了。而對於虛擬機中線程共享的區域,則須要進行垃圾回收,如堆和方法區,線程都會在這兩個區域產生自身的數據,佔據必定的內存大小,而且這些數據又可能會存在相互關聯的關係,因此,這部分的區域不像線程私有的區域那樣能夠簡單自動的進行垃圾回收,此部分區域的垃圾回收很是複雜,而垃圾回收也主要是針對這部分區域。安全

3、垃圾收集算法數據結構

  任何垃圾收集算法都必須作兩件事情。首先,它必須檢測出垃圾對象。其次,它必須回收垃圾對象所使用的堆空間並還給程序。那麼問題來了,如何檢測出一個對象是否爲垃圾對象呢?通常有兩種算法解決這個問題。1. 引用計數算法 2. 可達性分析算法。多線程

  1.引用計數算法併發

  堆中的每個對象有一個引用計數,當一個對象被建立,並把指向該對象的引用賦值給一個變量時,引用計數置爲1,當再把這個引用賦值給其餘變量時,引用計數加1,當一個對象的引用超過了生命週期或者被設置爲新值時,對象的引用計數減1,任何引用計數爲0的對象均可以被當成垃圾回收。當一個對象被回收時,它所引用的任何對象計數減1,這樣,可能會致使其餘對象也被當垃圾回收。ide

  問題:很難檢測出對象之間的額相互引用(引用循環問題)函數

  以下代碼段能夠從反面驗證虛擬機的垃圾回收不是採用的引用計數。oop

package com.leesf.chapter3;

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過
     */
    private byte[] bigSize = new byte[2 * _1MB];
    
    public static void testGC() {
        // 定義兩個對象
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        
        // 給對象的成員賦值,即存在相互引用狀況
        objA.instance = objB;
        objB.instance = objA;
        
        // 將引用設爲空,即沒有到堆對象的引用了
        objA = null;
        objB = null;
        
        // 進行垃圾回收
        System.gc();    
    }
    
    public static void main(String[] args) {
        testGC();    
    }
}
View Code

  代碼的運行參數設置爲: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

   在代碼objA = null 和 objB = null 以前,內存結構示意圖以下

  

  注意:局部變量區的第一項並無this引用,由於testGC方法是類方法。

  在代碼objA = null 和 objB = null 以後,內存結構示意圖以下

  

  objA和objB到堆對象的引用已經沒有了,可是ReferenceCountingGC對象內部還存在着循環引用,咱們在圖中也能夠看到。即使如此,JVM仍是把這兩個對象當成垃圾進行了回收。具體的GC日誌以下:

 

  由GC日誌可知發生了兩次GC,由11390K -> 514K,即對兩個對象都進行了回收,也從側面說明JVM的垃圾收集器不是採用的引用計數的算法來進行垃圾回收的。

  2.可達性分析算法

  此算法的基本思想就是選取一系列GCRoots對象做爲起點,開始向下遍歷搜索其餘相關的對象,搜索所走過的路徑成爲引用鏈,遍歷完成後,若是一個對象到GCRoots對象沒有任何引用鏈,則證實此對象是不可用的,能夠被當作垃圾進行回收。

  那麼問題又來了,如何選取GCRoots對象呢?在Java語言中,能夠做爲GCRoots的對象包括下面幾種:

    1. 虛擬機棧(棧幀中的局部變量區,也叫作局部變量表)中引用的對象。

    2. 方法區中的類靜態屬性引用的對象。

    3. 方法區中常量引用的對象。

    4. 本地方法棧中JNI(Native方法)引用的對象。

  下面給出一個GCRoots的例子,以下圖,爲GCRoots的引用鏈。

  由圖可知,obj八、obj九、obj10都沒有到GCRoots對象的引用鏈,即使obj9和obj10之間有引用鏈,他們仍是會被當成垃圾處理,能夠進行回收。

4、對象的內存佈局

  Java中咱們提到最多的應該就是對象,可是咱們真的瞭解對象嗎,對象在內存中的存儲佈局如何?對象的內存佈局以下圖所示

  

  幾點說明:1.Mark Word部分數據的長度在32位和64位虛擬機(未開啓壓縮指針)中分別爲32bit和64bit。而後對象須要存儲的運行時數據其實已經超過了32位、64位Bitmap結構所能記錄的限度,可是對象頭信息是與對象自身定義的數據無關的外存儲成本,Mark Word通常被設計爲非固定的數據結構,以便存儲更多的數據信息和複用本身的存儲空間。2.類型指針,即指向它的類元數據的指針,用於判斷對象屬於哪一個類的實例。3.實例數據存儲的是真正有效數據,如各類字段內容,各字段的分配策略爲longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同寬度的字段老是被分配到一塊兒,便於以後取數據。父類定義的變量會出如今子類前面。3.對齊填充部分僅僅起到佔位符的做用,並不是必須。

  說完對象的內存佈局,如今來講說對象的引用,當咱們在堆上建立一個對象實例後,如何對該對象進行操做呢?比如一個電視機,我如何操做電視機來收看不一樣的電視節目,顯然咱們須要使用到遙控,而虛擬機中就是使用到引用,即虛擬機棧中的reference類型數據來操做堆上的對象。如今主流的訪問方式有兩種:

  1. 使用句柄訪問對象。即reference中存儲的是對象句柄的地址,而句柄中包含了對象示例數據與類型數據的具體地址信息,至關於二級指針。

  2. 直接指針訪問對象。即reference中存儲的就是對象地址,至關於一級指針。

  兩種方式有各自的優缺點。當垃圾回收移動對象時,對於方式一而言,reference中存儲的地址是穩定的地址,不須要修改,僅須要修改對象句柄的地址;而對於方式二,則須要修改reference中存儲的地址。從訪問效率上看,方式二優於方式一,由於方式二隻進行了一次指針定位,節省了時間開銷,而這也是HotSpot採用的實現方式。下圖是句柄訪問與指針訪問的示意圖。

 

 5、對象的引用

  前面所談到的檢測垃圾對象的兩種算法都是基於對象引用。在Java語言中,將引用分爲強引用、軟引用、弱引用、虛引用四種類型。引用強度依次減弱。具體以下圖所示

  

 

  對於可達性分析算法而言,未到達的對象並不是是「非死不可」的,若要宣判一個對象死亡,至少須要經歷兩次標記階段。1. 若是對象在進行可達性分析後發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件爲是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視做沒必要要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法而且該finalize方法並無被執行過,那麼,這個對象會被放置在一個叫F-Queue的隊列中,以後會由虛擬機自動創建的、優先級低的Finalizer線程去執行,而虛擬機沒必要要等待該線程執行結束,即虛擬機只負責創建線程,其餘的事情交給此線程去處理。2.對F-Queue中對象進行第二次標記,若是對象在finalize方法中拯救了本身,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其餘變量,那麼在第二次標記的時候該對象將從「即將回收」的集合中移除,若是對象仍是沒有拯救本身,那就會被回收。以下代碼演示了一個對象如何在finalize方法中拯救了本身,然而,它只能拯救本身一次,第二次就被回收了。具體代碼以下

/*
 * 此代碼演示了兩點:
 * 1.對象能夠再被GC時自我拯救
 * 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次
 * */

public class FinalizeEscapeGC {
    public String name;
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public FinalizeEscapeGC(String name) {
        this.name = name;
    }

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        System.out.println(this);
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    @Override
    public String toString() {
        return name;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC("leesf");
        System.out.println(SAVE_HOOK);
        // 對象第一次拯救本身
        SAVE_HOOK = null;
        System.out.println(SAVE_HOOK);
        System.gc();
        // 由於finalize方法優先級很低,因此暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }

        // 下面這段代碼與上面的徹底相同,可是這一次自救卻失敗了
        // 一個對象的finalize方法只會被調用一次
        SAVE_HOOK = null;
        System.gc();
        // 由於finalize方法優先級很低,因此暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }
    }

}
View Code

  運行結果以下:

  leesf
  null
  finalize method executed!
  leesf
  yes, i am still alive :)
  no, i am dead : (

  由結果可知,該對象拯救了本身一次,第二次沒有拯救成功,由於對象的finalize方法最多被虛擬機調用一次。此外,從結果咱們能夠得知,一個堆對象的this(放在局部變量表中的第一項)引用會永遠存在,在方法體內能夠將this引用賦值給其餘變量,這樣堆中對象就能夠被其餘變量所引用,即不會被回收。

6、方法區的垃圾回收

  方法區的垃圾回收主要回收兩部份內容:1. 廢棄常量。2. 無用的類。既然進行垃圾回收,就須要判斷哪些是廢棄常量,哪些是無用的類。

  如何判斷廢棄常量呢?以字面量回收爲例,若是一個字符串「abc」已經進入常量池,可是當前系統沒有任何一個String對象引用了叫作「abc」的字面量,那麼,若是發生垃圾回收而且有必要時,「abc」就會被系統移出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。

  如何判斷無用的類呢?須要知足如下三個條件

    1. 該類的全部實例都已經被回收,即Java堆中不存在該類的任何實例。

    2. 加載該類的ClassLoader已經被回收。

    3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

  知足以上三個條件的類能夠進行垃圾回收,可是並非無用就被回收,虛擬機提供了一些參數供咱們配置。

7、垃圾收集算法

  垃圾收集的主要算法有以下幾種:

    1. 標記 - 清除算法

    2. 複製算法

    3. 標記 - 整理算法

    4. 分代收集算法

  7.1 標記 - 清除算法

  首先標記出全部須要回收的對象,使用可達性分析算法判斷一個對象是否爲可回收,在標記完成後統一回收全部被標記的對象。下圖是算法具體的一次執行過程後的結果對比。

  

  說明:1.效率問題,標記和清除兩個階段的效率都不高。2.空間問題,標記清除後會產生大量不連續的內存碎片,之後須要給大對象分配內存時,會提早觸發一次垃圾回收動做。

  7.2 複製算法

  將內存分爲兩等塊,每次使用其中一塊。當這一塊內存用完後,就將還存活的對象複製到另一個塊上面,而後再把已經使用過的內存空間一次清理掉。圖是算法具體的一次執行過程後的結果對比。

  說明:1.無內存碎片問題。2.可用內存縮小爲原來的一半。 3.當存活的對象數量不少時,複製的效率很慢。

  7.3 標記 - 整理算法

  標記過程仍是和標記 - 清除算法同樣,以後讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存,標記 - 整理算法示意圖以下

  

  說明:1.無需考慮內存碎片問題。

  7.4 分代收集算法

  把堆分爲新生代和老年代,而後根據各年代的特色選擇最合適的回收算法。在新生代基本上都是朝生暮死的,生存時間很短暫,所以能夠採擁標記 - 複製算法,只須要複製少許的對象就能夠完成收集。而老年代中的對象存活率高,也沒有額外的空間進行分配擔保,所以必須使用標記 - 整理或者標記 - 清除算法進行回收。

8、HotSpot的算法實現

  對於可達性分析而言,咱們知道,首先須要選取GCRoots結點,而GCRoots結點主要在全局性的引用(如常量或類靜態屬性)與執行上下文(如棧幀中的局部變量表)中。方法區能夠很大,這對於尋找GCRoots結點來講會很是耗時。當選取了GCRoots結點以後,進行可達性分析時必需要保證一致性,即在進行分析的過程當中整個執行系統看起來就好像被凍結在某個時間點上,不能夠在分析的時候,對象的關係還在動態變化,這樣的話分析的準確性就得不到保證,因此可達性分析是時間很是敏感的。

  爲了保證分析結果的準確性,就會致使GC進行時必須停頓全部Java執行線程(Stop the world),爲了儘量的減小Stop the world的時間,Java虛擬機使用了一組稱爲OopMap的數據結構,該數據結構用於存放對象引用的地址,這樣,進行可達性分析的時候就能夠直接訪問OopMap就能夠得到對象的引用,從而加快分析過程,減小Stop the world時間。

  OopMap數據結構有利於進行GC,是否是虛擬機不管什麼時候想要進行GC均可以進行GC,即不管虛擬機在執行什麼指令均可以進行GC?答案是否認的,由於要想讓虛擬機不管在執行什麼指令的時候均可以進行GC的話,須要爲每條指令都生成OopMap,顯然,這樣太浪費空間了。爲了節約寶貴的空間,虛擬機只在」特定的位置「存放了OopMap數據結構,這個特定的位置咱們稱之爲安全點。程序執行時並不是在全部地方都可以停頓下來開始GC(可達性分析),只有到達安全點的時候才能暫停。安全點能夠由方法調用、循環跳轉、異常跳轉等指令產生,由於這些指令會讓程序長時間執行。

  如今咱們已經知道了安全點的概念,即進行GC必需要到達安全點,那麼在發生GC時如何讓全部線程到達安全點再暫停呢?有兩種方法1. 搶先式中斷,在發生GC時,首先把全部線程所有中斷,若是發現線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。2. 主動式中斷,在發生GC時,不中斷線程,而是設置一個標誌,全部線程執行時主動輪詢這個標誌,發生標誌位真就本身中斷掛起,輪詢標誌的地方和安全點是重合的,也有多是建立對象須要分配內存的地方。

  如今問題又來了,當程序不執行的時候,如何讓全部線程達到安全點呢?典型的就是線程處於Sleep狀態或者Blocked狀態,這時候線程是沒法跑到安全點再中斷本身的,虛擬機也確定不可能等待該線程被喚醒並從新分配CPU時間後,跑到安全點再暫停。爲了解決這個問題,引安全區域的概念。安全區域是對安全點的擴展,能夠當作由不少安全點組成,安全區域是指一段代碼片斷之中,引用關係不會發生變化。在這個區域的任何地方開始GC都是安全的。當線程執行到安全區域的代碼時,首先標示本身已經進入了安全區域,那麼,在這段時間裏JVM發起GC時,就不用管標示本身爲安全區域狀態的線程了。在線程奧離開安全區域時,它要檢查系統是否已經完成了根節點枚舉(或者整個GC過程),若完成,線程繼續執行;不然,它必須等待直到收到能夠安全離開安全區域的信號。

9、垃圾收集器

  垃圾收集器是內存回收的具體實現,HotSpot虛擬機包含的全部收集器以下:

  

  說明:圖中存在連線表示能夠搭配使用,總共有7種不一樣分代的收集器。

  9.1 Serial收集器

  Serial收集器爲單線程收集器,在進行垃圾收集時,必需要暫停其餘全部的工做線程,直到它收集結束。運行過程以下圖所示

  

  說明:1. 須要STW(Stop The World),停頓時間長。2. 簡單高效,對於單個CPU環境而言,Serial收集器因爲沒有線程交互開銷,能夠獲取最高的單線程收集效率。

  9.2 ParNew收集器

  ParNew是Serial的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲與Serial徹底同樣,運行過程以下圖所示

  

  說明:1.Server模式下虛擬機的首選新生收集器,與CMS進行搭配使用。

  9.3 Parallel Scavenge收集器

  Parallel Scavenge收集器的目標是達到一個可控制的吞吐量,吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間),高吞吐量能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務,而且虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應調節策略。

  9.4 Serial Old收集器

  老年代的單線程收集器,使用標記 - 整理算法,運行過程在以前的Serial收集器已經給出。再也不累贅。

  9.5 Parallel Old收集器

  老年代的多線程收集器,使用標記 - 整理算法,吞吐量優先,適合於Parallel Scavenge搭配使用,運行過程以下圖所示

  

  9.6 CMS收集器

  CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時間爲目標的收集器。使用標記 - 清除算法,收集過程分爲以下四步:

    1. 初始標記,標記GCRoots能直接關聯到的對象,時間很短。

    2. 併發標記,進行GCRoots Tracing(可達性分析)過程,時間很長。

    3. 從新標記,修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,時間較長。

    4. 併發清除,回收內存空間,時間很長。

  其中,併發標記與併發清除兩個階段耗時最長,可是能夠與用戶線程併發執行。運行過程以下圖所示

  

  說明:1. 對CPU資源很是敏感,可能會致使應用程序變慢,吞吐率降低。2. 沒法處理浮動垃圾,由於在併發清理階段用戶線程還在運行,天然就會產生新的垃圾,而在這次收集中沒法收集他們,只能留到下次收集,這部分垃圾爲浮動垃圾,同時,因爲用戶線程併發執行,因此須要預留一部分老年代空間提供併發收集時程序運行使用。3. 因爲採用的標記 - 清除算法,會產生大量的內存碎片,不利於大對象的分配,可能會提早觸發一次Full GC。虛擬機提供了-XX:+UseCMSCompactAtFullCollection參數來進行碎片的合併整理過程,這樣會使得停頓時間變長,虛擬機還提供了一個參數配置,-XX:+CMSFullGCsBeforeCompaction,用於設置執行多少次不壓縮的Full GC後,接着來一次帶壓縮的GC。

  9.7 G1收集器

  能夠在新生代和老年代中只使用G1收集器。具備以下特色。

    1. 並行和併發。使用多個CPU來縮短Stop The World停頓時間,與用戶線程併發執行。

    2. 分代收集。獨立管理整個堆,可是可以採用不一樣的方式去處理新建立對象和已經存活了一段時間、熬過屢次GC的舊對象,以獲取更好的收集效果。

    3. 空間整合。基於標記 - 整理算法,無內存碎片產生。

    4. 可預測的停頓。能簡歷可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。

  使用G1收集器時,Java堆會被劃分爲多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但二者已經不是物理隔離了,都是一部分Region(不須要連續)的集合。G1收集器中,Region之間的對象引用以及其餘收集器的新生代和老年代之間的對象引用,虛擬機都使用Remembered Set來避免全堆掃描的。每一個Region對應一個Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查老年代的對象是否引用了新生代的對象),若是是,則經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中,當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會遺漏。

  對於上述過程咱們能夠看以下代碼加深理解

public class G1 {
    private Object obj;
    
    public init() {
        obj = new Object();
    }
    
    public static void main(String[] args) {
        G1 g1 = new G1();
        g1.init();
    }
}
View Code

  說明:程序中執行init函數的時候,會產生一個Write Barrier暫停中斷寫操做,此時,假定程序中G1對象與Object對象被分配在不一樣的Region當中,則會把obj的引用信息記錄在Object所屬的Remembered Set當中。具體的內存分佈圖以下

  

  若是不計算維護Remembered Set的操做,G1收集器的運做能夠分爲以下幾步

    1. 初始併發,標記GCRoots能直接關聯到的對象;修改TAMS(Next Top At Mark Start),使得下一階段程序併發時,可以在可用的Region中建立新對象,需停頓線程,耗時很短。

    2. 併發標記,從GCRoots開始進行可達性分析,與用戶程序併發執行,耗時很長。

    3. 最終標記,修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,變更的記錄將被記錄在Remembered Set Logs中,此階段會把其整合到Remembered Set中,須要停頓線程,與用戶程序並行執行,耗時較短。

    4. 篩選回收,對各個Region的回收價值和成本進行排序,根據用戶指望的GC時間進行回收,與用戶程序併發執行,時間用戶可控。

  G1收集器具體的運行示意圖以下

  各個垃圾回收器的介紹就到這裏,有興趣的讀者能夠去閱讀源碼。

  看到這裏,相信有些讀者對以前的GC日誌可能會有些疑惑,下面咱們來理解一下GC日誌

  [GC (System.gc()) [PSYoungGen: 6270K->584K(9216K)] 11390K->5712K(19456K), 0.0011969 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 584K->0K(9216K)] [ParOldGen: 5128K->514K(10240K)] 5712K->514K(19456K), [Metaspace: 2560K->2560K(1056768K)], 0.0059342 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
Heap
 PSYoungGen      total 9216K, used 82K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 1% used [0x00000000ff600000,0x00000000ff614920,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 514K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 5% used [0x00000000fec00000,0x00000000fec80928,0x00000000ff600000)
 Metaspace       used 2567K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

  這是以前出現過的GC日誌,能夠知道筆者虛擬機的垃圾收集器的組合爲Parallel Scavenge(新生代) + Parallel Old(老年代),是根據PSYoungGen和ParOldGen得知,不一樣的垃圾回收器的不一樣組成的新生代和老年代的名字也有所不一樣。虛擬機也提供了參數供咱們選擇不一樣的垃圾收集器。

  1. [GC (System.gc())]與[Full GC (System.gc())],說明垃圾收集的停頓類型,不是區分新生代GC和老年代GC的,若是有Full,則表示這次GC發生了Stop The World。

  2. PSYoungGen: 6270K->584K(9216K),表示,新生代:該內存區域GC前已使用容量 -> 該內存區域GC後已使用容量(該內存區域總容量)

  3. 11390K->5712K(19456K),表示,GC前Java堆已使用的容量 -> GC後Java堆已使用的容量(Java堆總容量)

  4. 0.0011969 secs,表示GC所佔用的時間,單位爲秒。

  5. [Times: user=0.00 sys=0.00, real=0.00 secs],表示GC的更具體的時間,user表明用戶態消耗的CPU時間,sys表明內核態消耗的CPU時間,real表明操做從開始到結束所通過的牆鍾時間。CPU時間與牆鍾時間的區別是,牆鍾時間包括各類非運算的等待耗時,如等待磁盤IO,等待線程阻塞,CPU時間則不包含這些耗時。當系統有多CPU或者多核時,多線程操做會疊加這些CPU時間,因此讀者看到user或者sys時間超過real時間也是很正常的。

10、內存分配與回收策略

  前面咱們已經詳細討論了內存回收,可是,咱們程序中生成的對象是如何進行分配的呢?對象的內存分配,絕大部分都是在堆上分配,少數通過JIT編譯後被拆散爲標量類型並間接在棧上分配。在堆上的分配又能夠有以下分配,主要在新生代的Eden區分配,若是啓動了本地線程分配緩衝,將按照線程優先在TLAB上分配,少數直接在老年代分配,虛擬機也提供了一些參數供咱們來控制對象內存空間的分配。

  堆的結構圖以下圖所示

  

  下面咱們將從應用程序的角度理解對象的分配。

  10.1 對象優先在Eden區分配

  對象一般在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,與Minor GC對應的是Major GC、Full GC。

  Minor GC:指發生在新生代的垃圾收集動做,很是頻繁,速度較快。

  Major GC:指發生在老年代的GC,出現Major GC,常常會伴隨一次Minor GC,同時Minor GC也會引發Major GC,通常在GC日誌中統稱爲GC,不頻繁。

  Full GC:指發生在老年代和新生代的GC,速度很慢,須要Stop The World。

  以下代碼片斷展現了GC的過程 

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
     * */
    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[] args) {
        testAllocation();
    }
}
View Code

  運行結果:

  [GC (Allocation Failure) [DefNew: 7130K->515K(9216K), 0.0048317 secs] 7130K->6659K(19456K), 0.0048809 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
 def new generation   total 9216K, used 4694K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  50% used [0x00000000ff500000, 0x00000000ff580fa0, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

  說明:新生代可用的空間爲9M = 8M(Eden容量) + 1M(一個survivor容量),分配完allocation一、allocation二、allocation3以後,沒法再分配allocation4,會發生分配失敗,則須要進行一次Minor GC,survivor to區域的容量爲1M,沒法容納總量爲6M的三個對象,則會經過擔保機制將allocation一、allocation二、allocation3轉移到老年代,而後再將allocation4分配在Eden區。

  10.2 大對象直接進入老年代

  須要大量連續內存空間的Java對象稱爲大對象,大對象的出現會致使提早觸發垃圾收集以獲取更大的連續的空間來進行大對象的分配。虛擬機提供了-XX:PretenureSizeThreadshold參數來設置大對象的閾值,超過閾值的對象直接分配到老年代。

  具體代碼以下

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:PretenureSizeThreshold=3145728(3M)
     * */
    
    public static void testPretenureSizeThreshold() {
        byte[] allocation4 = new byte[5 * _1MB];
    }
    
    public static void main(String[] args) {
        testPretenureSizeThreshold();
    }
}
View Code

  運行結果:

  Heap
 def new generation   total 9216K, used 1314K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed489d0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb00010, 0x00000000ffb00200, 0x0000000100000000)
 Metaspace       used 2567K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K
  說明:能夠看到5MB的對象直接分配在了老年代。

  10.3 長期存活的對象進入老年代

  每一個對象有一個對象年齡計數器,與前面的對象的存儲佈局中的GC分代年齡對應。對象出生在Eden區、通過一次Minor GC後仍然存活,並可以被Survivor容納,設置年齡爲1,對象在Survivor區每次通過一次Minor GC,年齡就加1,當年齡達到必定程度(默認15),就晉升到老年代,虛擬機提供了-XX:MaxTenuringThreshold來進行設置。

  具體代碼以下

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=1
        -XX:+PrintTenuringDistribution
     * */
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        testPretenureSizeThreshold();
    }
}
View Code

  運行結果:

  [GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:     790400 bytes,     790400 total
: 5174K->771K(9216K), 0.0050541 secs] 5174K->4867K(19456K), 0.0051088 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4867K->0K(9216K), 0.0015279 secs] 8963K->4867K(19456K), 0.0016327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4867K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffac0d30, 0x00000000ffac0e00, 0x0000000100000000)
 Metaspace       used 2562K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

  說明:發生了兩次Minor GC,第一次是在給allocation3進行分配的時候會出現一次Minor GC,此時survivor區域不能容納allocation2,可是能夠容納allocation1,因此allocation1將會進入survivor區域而且年齡爲1,達到了閾值,將在下一次GC時晉升到老年代,而allocation2則會經過擔保機制進入老年代。第二次發生GC是在第二次給allocation3分配空間時,這時,allocation1的年齡加1,晉升到老年代,這次GC也能夠清理出原來allocation3佔據的4MB空間,將allocation3分配在Eden區。因此,最後的結果是allocation一、allocation2在老年代,allocation3在Eden區。

  10.4 動態對象年齡判斷

  對象的年齡到達了MaxTenuringThreshold能夠進入老年代,同時,若是在survivor區中相同年齡全部對象大小的總和大於survivor區的一半,年齡大於等於該年齡的對象就能夠直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。

  具體代碼以下:

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=15
        -XX:+PrintTenuringDistribution
     * */
    
    public static void testTenuringThreshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        testPretenureSizeThreshold2();
    }
}
View Code

  運行結果:

  [GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1048576 bytes,    1048576 total
: 5758K->1024K(9216K), 0.0049451 secs] 5758K->5123K(19456K), 0.0049968 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 5120K->0K(9216K), 0.0016442 secs] 9219K->5123K(19456K), 0.0016746 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5123K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb00f80, 0x00000000ffb01000, 0x0000000100000000)
 Metaspace       used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

  結果說明:發生了兩次Minor GC,第一次發生在給allocation4分配內存時,此時allocation一、allocation2將會進入survivor區,而allocation3經過擔保機制將會進入老年代。第二次發生在給allocation4分配內存時,此時,survivor區的allocation一、allocation2達到了survivor區容量的一半,將會進入老年代,這次GC能夠清理出allocation4原來的4MB空間,並將allocation4分配在Eden區。最終,allocation一、allocation二、allocation3在老年代,allocation4在Eden區。

  10.5 空間分配擔保

  在發生Minor GC時,虛擬機會檢查老年代連續的空閒區域是否大於新生代全部對象的總和,若成立,則說明Minor GC是安全的,不然,虛擬機須要查看HandlePromotionFailure的值,看是否運行擔保失敗,若容許,則虛擬機繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若大於,將嘗試進行一次Minor GC;若小於或者HandlePromotionFailure設置不運行冒險,那麼此時將改爲一次Full GC,以上是JDK Update 24以前的策略,以後的策略改變了,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

  冒險是指通過一次Minor GC後有大量對象存活,而新生代的survivor區很小,放不下這些大量存活的對象,因此須要老年代進行分配擔保,把survivor區沒法容納的對象直接進入老年代。

  具體的流程圖以下:

                 

 

  具體代碼以下:

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:+HandlePromotionFailure
     * */
    
    public static void testHandlePromotion() {
        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7,
        allocation8;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation1 = null;
        allocation4 = new byte[2 * _1MB];
        allocation5 = new byte[2 * _1MB];
        allocation6 = new byte[2 * _1MB];
        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        allocation7 = new byte[2 * _1MB];
    }
    
    public static void main(String[] args) {
        testHandlePromotion();
    }
}
View Code

  運行結果:

  [GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     528280 bytes,     528280 total
: 7294K->515K(9216K), 0.0040766 secs] 7294K->4611K(19456K), 0.0041309 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 6818K->0K(9216K), 0.0012444 secs] 10914K->4611K(19456K), 0.0012760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000)
 Metaspace       used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

  說明:發生了兩次GC,第一次發生在給allocation4分配內存空間時,因爲老年代的連續可用空間大於存活的對象總和,因此allocation二、allocation3將會進入老年代,allocation1的空間將被回收,allocation4分配在新生代;第二次發生在給allocation7分配內存空間時,這次GC將allocation四、allocation五、allocation6所佔的內存所有回收。最後,allocation二、allocation3在老年代,allocation7在新生代。

11、總結

  至此,JVM垃圾收集部分就已經介紹完了,看完這部分咱們應該知道JVM是怎樣進行垃圾回收的,而且對JVM的理解更加加深。

  花了很長時間,終於寫完了這一部分,仍是收穫不少,在看的同時不斷記錄,更進一步加深了印象,感受還不錯,謝謝各位園友的觀看~

 

參考連接:http://www.open-open.com/lib/view/open1429883238291.html

參考文獻:深刻Java虛擬(原書第2版)、深刻理解Java虛擬機-JVM高級特性與最佳實踐

相關文章
相關標籤/搜索