JVM4:垃圾收集器和垃圾收集算法

前言

垃圾收集器主要考慮的工作是什麼內存該回收、什麼時候進行回收以及該怎麼樣進行回收?

通過之前的內容我們都知道,程序計數器、JVM棧及本地方法棧都是線程私有的,執行完畢自動銷燬,不需要過多考慮內存回收問題;而堆和方法區是屬於共享的區域,運行時創建的對象等信息都存放在這些區域中,垃圾收集器主要也是關注這部分內存的使用情況。

什麼內存該回收

垃圾收集器回收的是一些已經無用的對象,判斷對象無用的方法主要有:

  1. 引用計數法

    爲新建的對象添加引用計數器,每當有個地方引用它時計數器加1,引用失效時則減1。當回收時,發現計數器爲0,則回收此對象所佔用的內存。

    a.優點:實現簡單,效率高

    b.缺點:解決不了循環引用的場景。如:

    /**
     * Title: ReferemceCountGC
     * Description: vm args:-XX:+PrintHeapAtGC
     *
     * @author lin.xu
     * @date 2017/12/4.
     */
    public class ReferemceCountGC {
    
      public Object instance = null;
    
      private static final int _1MB = 1024 * 1024;
    
      private byte[] bigSize = new byte[2 * _1MB];
    
      public static void main(String args[]) {
        ReferemceCountGC objA = new ReferemceCountGC();
        ReferemceCountGC objB = new ReferemceCountGC();
    
        objA.instance = objB;
        objB.instance = objA;
    
        System.gc();
      }
    }
    
  2. 可達性分析法

    JVM根據選定的“GC Roots”,以“GC Roots”作爲起點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain)。當對象和“GC Roots”之間沒有引用鏈的時候,則表示對象不可達。

    不可達的對象會被進行第一次標記並進行篩選,篩選的條件是對象是否有必要執行finalize()方法。若沒有覆蓋finalize()方法或者finalize()方法已執行過,對象被第二次標記將等待下次回收。若對象覆蓋finalize()方法,對象將被放進F-Queue隊列中,由JVM開啓低優先級的Finalizer線程去執行,稍後GC將會在F-Queue中進行第二次標記。如果對象在finalize()方法中與引用鏈上的對象建立了聯繫,則被移除“即將回收”的集合。

    Java語言中,可以選擇如下對象作爲“GC Roots”:

    • 虛擬機棧(棧幀中的本地變量表)中引用的對象;
    • 方法區中類靜態屬性引用的對象;
    • 方法區中類常量引用的對象;
    • 本地方法棧中JNI引用的對象

對於判定對象是否可用與“引用”也有關。傳統的將引用分爲有引用與無引用,但是有些對象是可能接下來會有用的。所以JVM希望當內存空間足夠時,則保留這部分對象,如果進行回收後,內存還是不夠的話,則回收這些對象。Java對引用進行了擴充,分爲如下幾種:

  • 強引用(Strong Reference)

    類似Object obj = new Object()這類的引用,只要存在,收集器就不會回收此對象

  • 軟引用(Soft Reference)

    用來描述還有用但並非必需的對象。對於軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍進行第二次回收

  • 弱引用(Weak Reference)

    用來描述非必需對象。無論內存是否足夠,都會回收弱引用關聯的對象

  • 虛引用(Phantom Reference)

    稱爲幽靈引用或幻影引用。無法通過虛引用獲取對象實例,其唯一目的是能在這個對象被收集器回收時獲取系統通知

方法區也是可以進行對象回收的,其回收內容包括廢棄常量和無用的類。但相較於新生代,回收效率很低。回收廢棄量與Java堆中的對象回收類似,但是針對類的回收就要嚴格很多:

  • 該類所有的實例均已被回收,即Java類中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法通過反射來訪問該類的方法

怎樣進行回收

首先我們來看看回收的算法。

垃圾收集算法

  1. 標記-清除算法

    分爲標記和清除兩個步驟。首先標記需要回收的內存空間,之後將標記的內存空間進行回收。

    不足之處:效率問題,標記和清除效率都不高;空間問題,清除過程中由於未整理內存空間,使得內存空間裏很多碎片空間,存儲大對象時可能不得不進行一次Full GC操作。

  2. 複製算法

    將內存一分爲二,每次只使用其中一半。當其中一半內存快使用完時,將還存活的對象複製到另一半中內存中,把已使用的內存清空。通常用於新生代的回收。

  3. 標記-整理算法

    對內存進行標記,將存活對象都向內存一端移動,然後直接清理存活對象邊界外的內存空間。

  4. 分代收集算法

    根據內存區域進行不同的垃圾回收算法。新生代只有少量內存存活可以採用複製算法;老年代回收效率低可以採用標記-清除或者標記-整理算法。

HotSpot虛擬機的算法實現

  1. 枚舉根節點

    難點:

    1. 逐個檢查GC Roots到對象的引用會很耗時;
    2. 可達性分析對執行時間的敏感還體現在GC停頓上。因爲在枚舉根節點時,爲了保持一致性,需要停頓所有的Java執行線程。

    解決辦法:

    HotSpot在類加載完成後,採用OopMap的數據結構存儲對象內什麼偏移量上是什麼類型的數據,在JIT編譯過程中,也會在特定的位置記錄棧和寄存器中哪些位置是引用。GC掃描時可以依據OopMap來獲取信息。

  2. 安全點(Safe Point)

    有了OopMap可以快速枚舉出根節點,但是不能爲每個指令生成OopMap,否則開銷太大。HotSpot是在特定的位置記錄OopMap信息,這個特定的位置就指安全點。GC只有等所有線程執行到安全點的時候纔會停止用戶線程的執行。

    需要考慮的問題是如何讓線程跑到安全點時停止下來。兩種方案可選:

    1. 搶先式中斷(Preemptive Suspension)

      GC發生時,所有線程全部中斷,如果有線程不在安全點中斷,則恢復讓其繼續執行至安全點。現幾乎沒有虛擬機採用這種方式。

    2. 主動式中斷(Voluntary Suspension)

      GC需要執行的時候,會在安全點設置個標誌。當線程執行時,都去輪詢這個標誌,發現中斷標誌爲真時就主動掛起。

  3. 安全區域(Safe Region)

    安全點解決了運行時的線程GC問題。但是某些線程可能處在Sleep狀態或者Block狀態,那它不能響應中斷的請求。這樣HotSpot採用安全區域解決。安全區域是指在一段代碼片段之中,對象引用關係不會發生變化,這個區域任何地方都是GC安全的。

    具體過程就是:線程執行進入了Safe Region區域代碼中,標誌自己進入安全區域。當JVM發起GC時,忽略處於Safe Region的線程。當線程需要離開安全區域,首先要檢測GC是否完成,沒完成需要繼續等待,直到收到可以離開Safe Region區域的信號爲止。

垃圾收集器

  1. Serial收集器

    最基本、最悠久的收集器。用於新生代,單線程的收集器,需要Stop The World。適用於運行在Client模式下的JVM。

參數 描述
UseSerialGC 使用Serial + Serial Old的收集器組合進行內存回收
SurvivorRatio 新生代中Eden與Survivor區域的容量比值。默認爲8,代表Eden:Survivor=8:1
PretenureSizeThreshold 直接晉升到老年代的對象大小。設置這個參數後,大於這個參數的對象將直接分配到老年代
MaxTenuringThreshold 晉升到老年代的對象年齡。每個對象在堅持一次Minor GC後,年齡加1,當超過這個參數值則進入老年代
HandlerPromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有對象都存活的極端情況
  1. ParNew收集器

    Serial收集器的多線程版本。用於新生代,新生代回收並行,老年代回收串行;新生代複製算法,老年代標記-整理算法。適用於CPU多核的情形。

參數 描述
UseParNewGC 使用ParNew + Serial Old的收集器組合進行內存回收
ParallelGCThreads 設置並行GC時進行內存回收的線程數
SurvivorRatio 新生代中Eden與Survivor區域的容量比值。默認爲8,代表Eden:Survivor=8:1
PretenureSizeThreshold 直接晉升到老年代的對象大小。設置這個參數後,大於這個參數的對象將直接分配到老年代
MaxTenuringThreshold 晉升到老年代的對象年齡。每個對象在堅持一次Minor GC後,年齡加1,當超過這個參數值則進入老年代
HandlerPromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有對象都存活的極端情況
  1. Parallel Scavenge收集器

    類似於ParNew收集器,主要關注吞吐量。與ParNew收集器重要區別是:提供自適應的調節策略。當設置UseAdaptiveSizePolicy後,JVM會根據監控信息,動態調整新生代大小、Eden與Survivor比例、晉升老年代對象大小等參數以提供更合適的GC停頓時間或者最大的吞吐量。適合用於後臺運算不需要太多交互的任務。

參數 描述
UseParallelGC 虛擬機運行在Server模式下的默認值。打開此開關後,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行內存回收
MaxGCPauseMillis 設置GC的最大停頓時間
GCTimeRatio GC時間佔總時間的比率,默認值爲99,即允許1%的GC時間
ParallelGCThreads 設置並行GC時進行內存回收的線程數
SurvivorRatio 新生代中Eden與Survivor區域的容量比值。默認爲8,代表Eden:Survivor=8:1
PretenureSizeThreshold 直接晉升到老年代的對象大小。設置這個參數後,大於這個參數的對象將直接分配到老年代
MaxTenuringThreshold 晉升到老年代的對象年齡。每個對象在堅持一次Minor GC後,年齡加1,當超過這個參數值則進入老年代
HandlerPromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有對象都存活的極端情況
UseAdaptiveSizePolicy 動態調和智能Java堆中各個區域的大小以及進入老年代的年齡
  1. Serial Old收集器

    老年代、單線程收集器,採用“標記-整理”算法。主要適用於Client模式下;如果在Server模式,一是作爲Parallel Scavenge收集器搭配適用;一是當CMS在併發收集發生Concurrent Mode Failure時作爲後備預案使用

  2. Parallel Old收集器

    Parallel Scavenge收集器的老年代版本,多線程收集器,採用“標記-整理”算法。適用於注重吞吐量及CPU資源敏感的場合。

參數 描述
UseParallelOldGC 打開此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行內存回收
  1. CMS收集器

    CMS基於“標記-清除”算法實現,老年代收集器。適用於重視服務的響應速度,希望系統停頓時間越短,以給用戶更好體驗的場景。運作過程分以下4步:

    1. 初始標記:標記GC Roots能直接關聯到的對象。需要STOP THE WORLD;
    2. 併發標記:GC Roots Tracing的過程,依據GC Roots找出存活的對象;
    3. 重新標記:修正併發標記過程中由於用戶線程繼續執行而導致標記變動的部分。需要STOP THE WORLD;
    4. 併發清理

    優點:
    併發收集、低停頓

    缺點:

    1. 對CPU資源非常敏感。CMS啓動回收的線程數是(CPU數量+3)/4,當CPU數量很少時,回收線程佔用的資源越多,用戶線程就佔用資源越少;
    2. 無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次的Full GC。併發清理時用戶線程仍在執行,那就可能有垃圾產生。這部分垃圾(浮動垃圾)只能在下次收集過程中清理掉;在併發標記時還有用戶線程在執行,老年代中就可能被存入對象,如果老年代內存不足,則會拋出“Concurrent Mode Failure”,這時會臨時啓動Serail Old收集器來進行老年代的垃圾收集。
    3. 由於採用“標記-清理”算法,會產生大量空間碎片。
參數 描述
UseConcMarkSweepGC 打開此開關後,使用ParNew + CMS + Serial Old的收集器組合進行內存回收。Serial Old收集器將作爲CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用
CMSInitiatingOccupancyFraction 設置CMS收集器在老年代空間被使用多少後觸發垃圾回收。默認爲68%。
UseCMSCompactAtFullCollection 設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片整理
CMSFullGCsBeforeCompaction 設置CMS在進行若干次垃圾收集後再啓動一次內存碎片整理
  1. G1收集器

    G1是一款面向服務端應用的垃圾收集器,是當今收集器技術發展的最前沿成果之一。

    與其他收集器所不同的是,G1收集器是將堆劃分爲多個Region,每個Region中分配着新生代和老年代(不一定是連續空間)。G1收集器運作大致步驟如下:

    1. 初始標記(Initial Marking)

      標記下GC Roots能直接關聯的對象,並修改TAMS(Next Top at Mark Start)值,讓下一階段用戶線程併發執行時知道在正確的Region中存放對象。需要停止用戶線程。

      堆中每個Region都會維護一個Remembered Set,當有Reference寫入時,檢查引用的對象是否在同個Region中。如果是,則通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set中。這樣在枚舉根節點時可以不對全堆進行掃描。

    2. 併發標記(Concurrent Marking)

      從GC Roots出發,找出可到達的對象。

    3. 最終標記(Final Marking)

      修正在併發標記過程中由於用戶線程繼續執行導致變動的對象。JVM將併發標記過程中存儲在Remembered Set Logs中的記錄與Rembered Set合併,重新計算可到達的對象。並行執行。

    4. 篩選回收(Live Data Counting and Evacuation)

      對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。可併發並行。

    G1收集器特點如下:

    1. 併發與並行:併發標記,最終標記中的並行標記,並行回收
    2. 分代收集:新生代、老年代依然存在於G1收集器中
    3. 空間整合:整體上採用“標記-整理”算法,局部看(兩個Region之間)採用複製算法
    4. 可預測的停頓:G1在垃圾回收時會依據用戶指期望的停頓時間來選擇回收效率最高的Region

內存分配與回收策略

  1. 對象優先在Eden分配

  2. 大對象直接進入老年代

    對象大於PretenureSizeThreshold參數指定大小的對象直接進入老年代

  3. 長期存活的對象將進入老年代

    對象年齡超過MaxTenuringThreshold參數指定大小的對象直接進入老年代

  4. 動態對象年齡判斷

    如果在Survivor中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象直接進入老年代

  5. 空間分配擔保

    在發生Minor GC之前,需要判斷老年代可用連續空間是否大於新生代所有對象總空間。如果大於,直接進行Minor GC。如果不成立,則判斷是否設置了空間擔保(HandlePromotionFailure),若設置了空間擔保,則判斷老年代可用空間是否大於歷次存儲老年代的平均值,大於的話則進行MinorGC,不大於或者沒有設置空間擔保,需要進行Full GC。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章