1. 程式人生 > >JVM GC演算法

JVM GC演算法

在判斷哪些記憶體需要回收和什麼時候回收用到GC 演算法,本文主要對GC 演算法進行講解。

JVM垃圾判定演算法

常見的JVM垃圾判定演算法包括:引用計數演算法、可達性分析演算法

引用計數演算法(Reference Counting)

引用計數演算法是通過判斷物件的引用數量來決定物件是否可以被回收。

給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。

優點:簡單,高效,現在的objective-c用的就是這種演算法。

缺點:很難處理迴圈引用,相互引用的兩個物件則無法釋放。因此目前主流的Java虛擬機器都摒棄掉了這種演算法

舉個簡單的例子,物件objA和objB都有欄位instance,賦值令objA.instance=objB及objB.instance=objA,除此之外,這兩個物件沒有任何引用,實際上這兩個物件已經不可能再被訪問,但是因為互相引用,導致它們的引用計數都不為0,因此引用計數演算法無法通知GC收集器回收它們。

public class ReferenceCountingGC {
    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.gc();//GC
    }
}

執行結果

[GC (System.gc()) [PSYoungGen: 3329K->744K(38400K)] 3329K->752K(125952K), 0.0341414 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] 
[Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->628K(87552K)] 752K->628K(125952K), [Metaspace: 3450K->3450K(1056768K)], 0.0060728 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 998K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
  eden space 33280K, 3% used [0x00000000d5c00000,0x00000000d5cf9b20,0x00000000d7c80000)
  from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
  to   space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
 ParOldGen       total 87552K, used 628K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
  object space 87552K, 0% used [0x0000000081400000,0x000000008149d2c8,0x0000000086980000)
 Metaspace       used 3469K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

從執行結果看,GC日誌中包含“3329K->744K”,意味著虛擬機器並沒有因為這兩個物件互相引用就不回收它們,說明虛擬機器不是通過引用技術演算法來判斷物件是否存活的。

可達性分析演算法(根搜尋演算法)

可達性分析演算法是通過判斷物件的引用鏈是否可達來決定物件是否可以被回收。

從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜尋它們引用的物件,可以生成一棵引用樹,樹的節點視為可達物件,反之視為不可達。

可達性分析演算法

在Java語言中,可以作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中的引用物件。
  • 方法區中的類靜態屬性引用的物件。
  • 方法區中的常量引用的物件。
  • 本地方法棧中JNI(Native方法)的引用物件

真正標記以為物件為可回收狀態至少要標記兩次。

四種引用

強引用就是指在程式程式碼之中普遍存在的,類似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件

Object obj = new Object();

軟引用是用來描述一些還有用但並非必需的物件,對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK1.2之後,提供了SoftReference類來實現軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);

弱引用也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件,只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK1.2之後,提供了WeakReference類來實現弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);

虛引用也成為幽靈引用或者幻影引用,它是最弱的一中引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。在JDK1.2之後,提供給了PhantomReference類來實現虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);

JVM垃圾回收演算法

常見的垃圾回收演算法包括:標記-清除演算法,複製演算法,標記-整理演算法,分代收集演算法

在介紹JVM垃圾回收演算法前,先介紹一個概念。

Stop-the-World

Stop-the-world意味著 JVM由於要執行GC而停止了應用程式的執行,並且這種情形會在任何一種GC演算法中發生。當Stop-the-world發生時,除了GC所需的執行緒以外,所有執行緒都處於等待狀態直到GC任務完成。事實上,GC優化很多時候就是指減少Stop-the-world發生的時間,從而使系統具有高吞吐 、低停頓的特點。

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

之所以說標記/清除演算法是幾種GC演算法中最基礎的演算法,是因為後續的收集演算法都是基於這種思路並對其不足進行改進而得到的。標記/清除演算法的基本思想就跟它的名字一樣,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

標記階段:標記的過程其實就是前面介紹的可達性分析演算法的過程,遍歷所有的GC Roots物件,對從GC Roots物件可達的物件都打上一個標識,一般是在物件的header中,將其記錄為可達物件;

清除階段:清除的過程是對堆記憶體進行遍歷,如果發現某個物件沒有被標記為可達物件(通過讀取物件header資訊),則將其回收。

不足:

  • 標記和清除過程效率都不高
  • 會產生大量碎片,記憶體碎片過多可能導致無法給大物件分配記憶體。

標記-清除

複製演算法(Copying)

將記憶體劃分為大小相等的兩塊,每次只使用其中一塊,當這一塊記憶體用完了就將還存活的物件複製到另一塊上面,然後再把使用過的記憶體空間進行一次清理。

現在的商業虛擬機器都採用這種收集演算法來回收新生代,但是並不是將記憶體劃分為大小相等的兩塊,而是分為一塊較大的 Eden 空間和兩塊較小的 Survior 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活著的物件一次性複製到另一塊 Survivor 空間上,最後清理 Eden 和 使用過的那一塊 Survivor。HotSpot 虛擬機器的 Eden 和 Survivor 的大小比例預設為 8:1,保證了記憶體的利用率達到 90 %。如果每次回收有多於 10% 的物件存活,那麼一塊 Survivor 空間就不夠用了,此時需要依賴於老年代進行分配擔保,也就是借用老年代的空間。

不足:

  • 將記憶體縮小為原來的一半,浪費了一半的記憶體空間,代價太高;如果不想浪費一半的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。
  • 複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。

複製

標記—整理演算法(Mark-Compact)

標記—整理演算法和標記—清除演算法一樣,但是標記—整理演算法不是把存活物件複製到另一塊記憶體,而是把存活物件往記憶體的一端移動,然後直接回收邊界以外的記憶體,因此其不會產生記憶體碎片。標記—整理演算法提高了記憶體的利用率,並且它適合在收集物件存活時間較長的老年代。

不足:

效率不高,不僅要標記存活物件,還要整理所有存活物件的引用地址,在效率上不如複製演算法。

標記—整理

分代收集演算法(Generational Collection)

分代回收演算法實際上是把複製演算法和標記整理法的結合,並不是真正一個新的演算法,一般分為:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要進行回收的,新生代就是有很多的記憶體空間需要回收,所以不同代就採用不同的回收演算法,以此來達到高效的回收演算法。

新生代:由於新生代產生很多臨時物件,大量物件需要進行回收,所以採用複製演算法是最高效的。

老年代:回收的物件很少,都是經過幾次標記後都不是可回收的狀態轉移到老年代的,所以僅有少量物件需要回收,故採用標記清除或者標記整理演算法