1. 程式人生 > >JVM之GC演算法和種類

JVM之GC演算法和種類

垃圾收集演算法

這裡說說5種GC演算法,在說GC演算法前,先談談判斷物件是否活著的可達性分析演算法

  • 可達性分析演算法
  • 引用計數演算法
  • 標記-清除演算法
  • 標記-整理演算法
  • 複製演算法
  • 分代收集演算法

可達性分析演算法

垃圾回收,首先要判斷物件是否還活著.

Java通過可達性分析(Reachability Analysis)來判定物件是否存活

演算法基本思路:
- 通過一系列稱為”GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋走過的路徑,叫做引用鏈(Reference Chain)
- 當一個物件到GC Roots沒有任何引用鏈相連,從圖論來說,就是從GC Roots到物件不可達,此時判定物件不可用

Java中,可作為GC Roots的物件包括四種

  • VM Stack(棧幀中的本地變量表)中引用的物件
  • 本地方法棧中JNI(即Navtive方法)引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件

建立–>可達<–>可恢復–>不可達–>垃圾回收

強引用StringReference

  • 建立物件,並把物件賦給一個引用變數,程式通過這個引用變數來操作物件,物件和陣列都採用了強引用。

  • 一個物件被一個或以上引用變數引用,則處於可達狀態,不會被GC

軟引用SoftReference

  • 要用java.lang.ref.SoftReference實現
  • 描述有用但非必需的物件
  • 記憶體空間足夠時,不會被GC;否則,可能會被GC
  • 常用於對記憶體敏感的程式中
  • get()獲取引用的物件

弱引用WeakReference

  • java.lang.ref.WeakReference實現
  • 描述非必需物件
  • 不管記憶體是否足夠,都會被GC
  • get()獲取引用的物件

虛引用PhantomReference

  • java.lang.ref.PhantomRefernce必須和java.lang.ref.RefernceQueue引用佇列聯合使用
  • 類似於沒有引用,和沒有引用的效果大致相同
  • 主要用於跟蹤物件被垃圾回收的狀態
  • !!! get()無法獲取引用的物件
  • GC後,虛引用會被放入引用佇列

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”,這時還處於“緩刑”狀態,真正標記一個物件的死亡,至少經歷兩次標記過程

  • 物件在可達性分析後,發現沒有GC Roots相連線的引用鏈,此時被第一次標記,且進行一次篩選
    • 篩選的條件是:是否有必要執行物件的finalize()方法
    • 物件沒有重寫finalze()方法,或者finalize()已經被虛擬機器呼叫過–>沒必要執行
  • 如果被判定為“有必要執行”,那麼物件會被放到一個叫做F-Queue的佇列中,並在稍後由一個JVM自動建立的、低優先順序的Finalizer執行緒去執行finalize()方法.
  • 稍後,GC將對F-Queue中的物件進行第二次小規模的標記. 如果物件沒有在finalize()中復活,那基本真的被回收了

引用計數演算法(Reference Counting)

  • 老牌的垃圾回收演算法

  • 給物件新增一個引用計數器,有一個地方引用它時,計數器+1;引用失效時,-1;計數器為0,物件就是不可能再被使用

  • 客觀來說,實現簡單,判定效率高

  • 應用如:微軟的Component Object Model技術,Python,ActionScript3

  • 但主流Java虛擬機器沒有用,因為很難解決物件間的迴圈引用問題

  • 引用和去引用伴隨+,-,影響效能

這涉及到如何判定物件是否可達,或者說是否活著,被引用著

標記-清除演算法(Mark-Sweep)

  • 最基礎的演算法,分為“標記”,“清除”兩個階段
  • 這是所有收集演算法的基礎,後續演算法都是基於這種思路,並對其不足進行改進而得到的。
  • 首先,標記出需要回收的物件
  • 接著,標記完成後,統一回收所有標記的物件

  • 主要問題:

    • 效率: 標記和清除兩個過程的效率都不高
    • 空間碎片: 標記清除後會產生大量不連續的記憶體碎片,碎片過多會導致大物件無法分配到足夠的連續記憶體,從而不得不提前觸發GC,甚至Stop The World

複製演算法(Copying)

  • 為解決效率問題,複製演算法出現了

  • 將記憶體分為大小相等的兩塊,每次只用一塊

  • 在一塊記憶體中,當它用完了,就把還或者的物件複製到另外一塊大小相等的記憶體中,連續儲存;同時,一次過清理已經使用過的記憶體空間
  • 相當於每次都回收整個半區,不用考慮碎片問題,只需移動堆頂指標,按順序分配,實現簡單,執行高效
  • 主要問題
    • 效率: 在物件存活率較高時,複製操作次數多,效率降低
    • 空間: 記憶體縮小了一半;需要額外空間做分配擔保(老年代)
  • From Survivor, To Survivor使用的就是複製演算法。老年代不使用這種演算法

標記-整理(Mark-Compact)

  • 根據老年代的特點,提出了標記-整理/壓縮演算法

  • 標記階段與標記-清除演算法一樣

  • 後續,讓存活物件都向一端移動,對齊,直接清理掉端邊界以外的記憶體
  • 老年代使用的演算法

分代收集演算法(Generational Collection)

當前的商業虛擬機器的垃圾回收都採用 分代收集 演算法。根據物件存活週期的不同,將記憶體劃分為幾塊。

一般將Java堆分為新生代和老年代。

新生代: 每次GC都有大批物件死去,只有少量存活

  • 使用複製演算法,只需要複製少量存活的物件

老年代:對此存活率高,沒有額外空間做分配擔保

  • 使用 標記-清除,標記-整理兩種演算法

HotSpot的演算法實現

首先,要解決的,是可達性分析,從GC Roots中找物件是否有可達的引用鏈。

列舉GC Roots根節點

可做GC Roots節點的,上文說過,主要有 全域性性的引用(包括常量或類靜態屬性),執行上下文(棧幀中的本地變量表和JNI引用的物件). 問題是,現在的應用,僅方法區就數百Megabyte,如果逐個檢查這裡邊的引用,必然消耗很多時間

可達性分析必須在一個能確保一致性的記憶體快照中進行,分析期間,記憶體系統不能出現變化,否則,會出現分析不正確的情況,例如,前腳這邊標記了A物件不可達,此時記憶體系統還在執行,萬一A又被引用了,但A已經被標記為不可達。所以,得有GC停頓,號稱幾乎不會GC停頓的CMS收集器,在列舉根節點時,也必須停頓

目前主流VM使用準確式GC,所以不需要一個不漏地檢查完所有的全域性和上下文的引用的位置,虛擬機器有辦法直接得知哪些地方存放著物件的引用

HotSpot的實現中,使用一組叫做OopMap的資料結構來達到目的。

(1)類載入完成時,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來。
(2)在JIT編譯過程中,會在特定的位置記錄下棧和暫存器中,哪些位置是引用

這樣,GC掃描時,就可以直接得知這些資訊了

安全點(SafePoint)

在OopMap(Ordinary Object Pointer Map)的協助下,HotSpot可以快速準確完成GC Roots列舉。

問題是,引用關係變化,OopMap內容變化的指令非常多,如果為每一條指令生成對應的OopMap,那會需要有大量的空間,GC的空間成本會變得很高

事實上,HotSpot沒有為每條指令生成OopMap,前面說了,是在“特定位置”,這個特定位置,就是安全點SafePoint. 程式執行不是所有地方都停頓下來GC,而是到安全點,才停頓

安全點選定

所以,安全點的選定,不能太過少,否則GC等待時間太長,產生OOM;也不能太過頻繁,否則會增大執行時的負荷。

因此,安全點選擇的準則:

  • 是否具有讓程式長時間執行的特徵

具有這個特徵的就是指令序列複用,例如方法呼叫,迴圈跳轉,異常跳轉。所以,具有這些功能的指令才會產生SafePoint

讓所有執行緒跑到最近的安全點

另一個問題,發生GC時,如何讓所有執行緒跑到最近的安全點,除了JNI呼叫的執行緒。

兩種方案

  • 搶佔式中斷(Preemptive Suspension)
    • GC來時,先中斷執行緒,若發現執行緒不在安全點上,恢復執行緒,讓它跑的安全點
    • 現在,幾乎沒有VM使用這種方式
  • 主動式中斷(Voluntary Suspension)
    • 不主動操作執行緒,GC來時,簡單地設個標記,各個執行緒執行時主動輪詢這個標記,發現為真時,自己中斷掛起
    • 輪詢標記的地方和安全點重合,另外加上建立物件時需要分配記憶體的地方

安全區域(Safe Region)

SafePoint只是解決獲得CPU執行時間的執行緒,在不太長的時間內,可以遇到GC的SafePoint的問題。

但沒有分配到CPU執行時間的執行緒,如Sleep,Block狀的執行緒,它們無法響應JVM的中斷請求,跑到SafePoint中斷掛起,JVM顯然也不會等待執行緒重新獲得CPU時間

對這一問題,需要安全區域Safe Region

安全區域

  • 一段程式碼片段中,引用關係不會發生變化
  • 這個區域之中,任意地方開始GC,都是安全的
  • 可以看作SafePoint的擴充套件

當執行緒執行到安全區域中的程式碼時,首先標識自己已經進入Safe Region。那麼,這段時間裡JVM發起GC時,就不用管標識自己在Safe Region狀態的執行緒了,因為這些執行緒的引用關係不會發生變化。

執行緒離開Safe Region時,要檢查系統是否已經完成GC Roots列舉或者整個GC過程。如果沒有,就必須等待,直到收到可以安全離開的訊號為止;如果完成了,執行緒繼續執行。