1. 程式人生 > >深入理解JVM虛擬機器2:JVM垃圾回收基本原理和演算法

深入理解JVM虛擬機器2:JVM垃圾回收基本原理和演算法

JVM GC基本原理與GC演算法 

Java的記憶體分配與回收全部由JVM垃圾回收程序自動完成。與C語言不同,Java開發者不需要自己編寫程式碼實現垃圾回收。這是Java深受大家歡迎的眾多特性之一,能夠幫助程式設計師更好地編寫Java程式。 

下面四篇教程是瞭解Java 垃圾回收(GC)的基礎:

這篇教程是系列第一部分。首先會解釋基本的術語,比如JDK、JVM、JRE和HotSpotVM。接著會介紹JVM結構和Java 堆記憶體結構。理解這些基礎對於理解後面的垃圾回收知識很重要。

Java關鍵術語

  • JavaAPI:一系列幫助開發者建立Java應用程式的封裝好的庫。
  • Java 開發工具包 (JDK):一系列工具幫助開發者建立Java應用程式。JDK包含工具編譯、執行、打包、分發和監視Java應用程式。
  • Java 虛擬機器(JVM):JVM是一個抽象的計算機結構。Java程式根據JVM的特性編寫。JVM針對特定於作業系統並且可以將Java指令翻譯成底層系統的指令並執行。JVM確保了Java的平臺無關性。
  • Java 執行環境(JRE):JRE包含JVM實現和Java API。

Java HotSpot 虛擬機器

每種JVM實現可能採用不同的方法實現垃圾回收機制。在收購SUN之前,Oracle使用的是JRockit JVM,收購之後使用HotSpot JVM。目前Oracle擁有兩種JVM實現並且一段時間後兩個JVM實現會合二為一。

HotSpot JVM是目前Oracle SE平臺標準核心元件的一部分。在這篇垃圾回收教程中,我們將會了解基於HotSpot虛擬機器的垃圾回收原則。

JVM體系結構

下面圖片總結了JVM的關鍵元件。在JVM體系結構中,與垃圾回收相關的兩個主要元件是堆記憶體和垃圾回收器。堆記憶體是記憶體資料區,用來儲存執行時的物件例項。垃圾回收器也會在這裡操作。現在我們知道這些元件是如何在框架中工作的。

 

 Java堆記憶體

我們有必要了解堆記憶體在JVM記憶體模型的角色。在執行時,Java的例項被存放在堆記憶體區域。當一個物件不再被引用時,滿足條件就會從堆記憶體移除。在垃圾回收程序中,這些物件將會從堆記憶體移除並且記憶體空間被回收。堆記憶體以下三個主要區域:

 

  1. 新生代(Young Generation)
    • Eden空間(Eden space,任何例項都通過Eden空間進入執行時記憶體區域)
    • S0 Survivor空間(S0 Survivor space,存在時間長的例項將會從Eden空間移動到S0 Survivor空間)
    • S1 Survivor空間 (存在時間更長的例項將會從S0 Survivor空間移動到S1 Survivor空間)
  2. 老年代(Old Generation)例項將從S1提升到Tenured(終身代)
  3. 永久代(Permanent Generation)包含類、方法等細節的元資訊

Java 垃圾回收是一項自動化的過程,用來管理程式所使用的執行時記憶體。通過這一自動化過程,JVM 解除了程式設計師在程式中分配和釋放記憶體資源的開銷。

啟動Java垃圾回收

作為一個自動的過程,程式設計師不需要在程式碼中顯示地啟動垃圾回收過程。System.gc()Runtime.gc()用來請求JVM啟動垃圾回收。

雖然這個請求機制提供給程式設計師一個啟動 GC 過程的機會,但是啟動由 JVM負責。JVM可以拒絕這個請求,所以並不保證這些呼叫都將執行垃圾回收。啟動時機的選擇由JVM決定,並且取決於堆記憶體中Eden區是否可用。JVM將這個選擇留給了Java規範的實現,不同實現具體使用的演算法不盡相同。

毋庸置疑,我們知道垃圾回收過程是不能被強制執行的。我剛剛發現了一個呼叫System.gc()有意義的場景。通過這篇文章瞭解一下適合呼叫System.gc() 這種極端情況。

各種GC的觸發時機(When)

GC型別

說到GC型別,就更有意思了,為什麼呢,因為業界沒有統一的嚴格意義上的界限,也沒有嚴格意義上的GC型別,都是左邊一個教授一套名字,右邊一個作者一套名字。為什麼會有這個情況呢,因為GC型別是和收集器有關的,不同的收集器會有自己獨特的一些收集型別。所以作者在這裡引用R大關於GC型別的介紹,作者覺得還是比較妥當準確的。如下:

  • Partial GC:並不收集整個GC堆的模式
    • Young GC(Minor GC):只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
    • Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
  • Full GC(Major GC):收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

觸發時機 

上面大家也看到了,GC型別分分類是和收集器有關的,那麼當然了,對於不同的收集器,GC觸發時機也是不一樣的,作者就針對預設的serial GC來說:

  • young GC:當young gen中的eden區分配滿的時候觸發。注意young GC中有部分存活物件會晉升到old gen,所以young GC後old gen的佔用量通常會有所升高。
  • full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。

FULL GC觸發條件詳解

除直接呼叫System.gc外,觸發Full GC執行的情況有如下四種。

1. 舊生代空間不足

舊生代空間只有在新生代物件轉入及建立為大物件、大陣列時才會出現不足的現象,當執行Full GC後空間仍然不足,則丟擲如下錯誤:

java.lang.OutOfMemoryError: Java heap space 

為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓物件在Minor GC階段被回收、讓物件在新生代多存活一段時間及不要建立過大的物件及陣列。

2. Permanet Generation空間滿

Permanet Generation中存放的為一些class的資訊等,當系統中要載入的類、反射的類和呼叫的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會丟擲如下錯誤資訊:

java.lang.OutOfMemoryError: PermGen space 

為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。

3. CMS GC時出現promotion failed和concurrent mode failure

對於採用CMS進行舊生代GC的程式而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。

promotion failed是在進行Minor GC時,survivor space放不下、物件只能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有物件要放入舊生代,而此時舊生代空間不足造成的。

應對措施為:增大survivor space、舊生代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢後很久才觸發sweeping動作。對於這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(單位為ms)來避免。

4. 統計得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間

這是一個較為複雜的觸發情況,Hotspot為了避免由於新生代物件晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程式第一次觸發Minor GC後,有6MB的物件晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。

當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。

除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc。

總結一下就是:

Minor GC ,Full GC 觸發條件

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

(1)呼叫System.gc時,系統建議執行Full GC,但是不必然執行

(2)老年代空間不足

(3)方法去空間不足

(4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體

(5)由Eden區、From Space區向To Space區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小。

什麼是Stop the world。

      Java中Stop-The-World機制簡稱STW,是在執行垃圾收集演算法時,Java應用程式的其他所有執行緒都被掛起(除了垃圾收集幫助器之外)。Java中一種全域性暫停現象,全域性停頓,所有Java程式碼停止,native程式碼可以執行,但不能與JVM互動;這些現象多半是由於gc引起。

GC時的Stop the World(STW)是大家最大的敵人。但可能很多人還不清楚,除了GC,JVM下還會發生停頓現象。

JVM裡有一條特殊的執行緒--VM Threads,專門用來執行一些特殊的VM Operation,比如分派GC,thread dump等,這些任務,都需要整個Heap,以及所有執行緒的狀態是靜止的,一致的才能進行。所以JVM引入了安全點(Safe Point)的概念,想辦法在需要進行VM Operation時,通知所有的執行緒進入一個靜止的安全點。

除了GC,其他觸發安全點的VM Operation包括:

1. JIT相關,比如Code deoptimization, Flushing code cache ;

2. Class redefinition (e.g. javaagent,AOP程式碼植入的產生的instrumentation) ;

3. Biased lock revocation 取消偏向鎖 ;

4. Various debug operation (e.g. thread dump or deadlock check);

Java垃圾回收過程

垃圾回收是一種回收無用記憶體空間並使其對未來例項可用的過程。

Eden 區:當一個例項被建立了,首先會被儲存在堆記憶體年輕代的 Eden 區中。

注意:如果你不能理解這些詞彙,我建議你閱讀這篇 垃圾回收介紹這篇教程詳細地介紹了記憶體模型、JVM 架構以及這些術語。

Survivor 區(S0 和 S1):作為年輕代 GC(Minor GC)週期的一部分,存活的物件(仍然被引用的)從 Eden 區被移動到 Survivor 區的 S0 中。類似的,垃圾回收器會掃描 S0 然後將存活的例項移動到 S1 中。

(譯註:此處不應該是Eden和S0中存活的都移到S1麼,為什麼會先移到S0再從S0移到S1?)

死亡的例項(不再被引用)被標記為垃圾回收。根據垃圾回收器(有四種常用的垃圾回收器,將在下一教程中介紹它們)選擇的不同,要麼被標記的例項都會不停地從記憶體中移除,要麼回收過程會在一個單獨的程序中完成。

老年代: 老年代(Old or tenured generation)是堆記憶體中的第二塊邏輯區。當垃圾回收器執行 Minor GC 週期時,在 S1 Survivor 區中的存活例項將會被晉升到老年代,而未被引用的物件被標記為回收。

老年代 GC(Major GC):相對於 Java 垃圾回收過程,老年代是例項生命週期的最後階段。Major GC 掃描老年代的垃圾回收過程。如果例項不再被引用,那麼它們會被標記為回收,否則它們會繼續留在老年代中。

記憶體碎片:一旦例項從堆記憶體中被刪除,其位置就會變空並且可用於未來例項的分配。這些空出的空間將會使整個記憶體區域碎片化。為了例項的快速分配,需要進行碎片整理。基於垃圾回收器的不同選擇,回收的記憶體區域要麼被不停地被整理,要麼在一個單獨的GC程序中完成。

垃圾回收中例項的終結

在釋放一個例項和回收記憶體空間之前,Java 垃圾回收器會呼叫例項各自的 finalize() 方法,從而該例項有機會釋放所持有的資源。雖然可以保證 finalize() 會在回收記憶體空間之前被呼叫,但是沒有指定的順序和時間。多個例項間的順序是無法被預知,甚至可能會並行發生。程式不應該預先調整例項之間的順序並使用 finalize() 方法回收資源。

  • 任何在 finalize過程中未被捕獲的異常會自動被忽略,然後該例項的 finalize 過程被取消。
  • JVM 規範中並沒有討論關於弱引用的垃圾回收機制,也沒有很明確的要求。具體的實現都由實現方決定。
  • 垃圾回收是由一個守護執行緒完成的。

物件什麼時候符合垃圾回收的條件?

  • 所有例項都沒有活動執行緒訪問。
  • 沒有被其他任何例項訪問的迴圈引用例項。

Java 中有不同的引用型別。判斷例項是否符合垃圾收集的條件都依賴於它的引用型別。 

引用型別 垃圾收集
強引用(Strong Reference) 不符合垃圾收集
軟引用(Soft Reference) 垃圾收集可能會執行,但會作為最後的選擇
弱引用(Weak Reference) 符合垃圾收集
虛引用(Phantom Reference) 符合垃圾收集

在編譯過程中作為一種優化技術,Java 編譯器能選擇給例項賦 null 值,從而標記例項為可回收。

class Animal {

    public static void main(String[] args) {

        Animal lion = new Animal();

        System.out.println("Main is completed.");

    }

 

    protected void finalize() {

        System.out.println("Rest in Peace!");

    }

}

在上面的類中,lion 物件在例項化行後從未被使用過。因此 Java 編譯器作為一種優化措施可以直接在例項化行後賦值lion = null。因此,即使在 SOP 輸出之前, finalize 函式也能夠打印出 'Rest in Peace!'。我們不能證明這確定會發生,因為它依賴JVM的實現方式和執行時使用的記憶體。然而,我們還能學習到一點:如果編譯器看到該例項在未來再也不會被引用,能夠選擇並提早釋放例項空間。

  • 關於物件什麼時候符合垃圾回收有一個更好的例子。例項的所有屬效能被儲存在暫存器中,隨後暫存器將被訪問並讀取內容。無一例外,這些值將被寫回到例項中。雖然這些值在將來能被使用,這個例項仍然能被標記為符合垃圾回收。這是一個很經典的例子,不是嗎?
  • 當被賦值為null時,這是很簡單的一個符合垃圾回收的示例。當然,複雜的情況可以像上面的幾點。這是由 JVM 實現者所做的選擇。目的是留下儘可能小的記憶體佔用,加快響應速度,提高吞吐量。為了實現這一目標, JVM 的實現者可以選擇一個更好的方案或演算法在垃圾回收過程中回收記憶體空間。
  • 當 finalize() 方法被呼叫時,JVM 會釋放該執行緒上的所有同步鎖。

GC Scope 示例程式

Class GCScope {

    GCScope t;

    static int i = 1;

 

    public static void main(String args[]) {

        GCScope t1 = new GCScope();

        GCScope t2 = new GCScope();

        GCScope t3 = new GCScope();

 

        // No Object Is Eligible for GC

 

        t1.t = t2; // No Object Is Eligible for GC

        t2.t = t3; // No Object Is Eligible for GC

        t3.t = t1; // No Object Is Eligible for GC

 

        t1 = null;

        // No Object Is Eligible for GC (t3.t still has a reference to t1)

 

        t2 = null;

        // No Object Is Eligible for GC (t3.t.t still has a reference to t2)

 

        t3 = null;

        // All the 3 Object Is Eligible for GC (None of them have a reference.

        // only the variable t of the objects are referring each other in a

        // rounded fashion forming the Island of objects with out any external

        // reference)

    }

 

    protected void finalize() {

        System.out.println("Garbage collected from object" + i);

        i++;

    }

 

class GCScope {

    GCScope t;

    static int i = 1;

 

    public static void main(String args[]) {

        GCScope t1 = new GCScope();

        GCScope t2 = new GCScope();

        GCScope t3 = new GCScope();

 

        // 沒有物件符合GC

        t1.t = t2; // 沒有物件符合GC

        t2.t = t3; // 沒有物件符合GC

        t3.t = t1; // 沒有物件符合GC

 

        t1 = null;

        // 沒有物件符合GC (t3.t 仍然有一個到 t1 的引用)

 

        t2 = null;

        // 沒有物件符合GC (t3.t.t 仍然有一個到 t2 的引用)

 

        t3 = null;

        // 所有三個物件都符合GC (它們中沒有一個擁有引用。

        // 只有各物件的變數 t 還指向了彼此,

        // 形成了一個由物件組成的環形的島,而沒有任何外部的引用。)

    }

 

    protected void finalize() {

        System.out.println("Garbage collected from object" + i);

        i++;

    }

 

在判斷哪些記憶體需要回收和什麼時候回收用到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),老年代就是很少垃圾需要進行回收的,新生代就是有很多的記憶體空間需要回收,所以不同代就採用不同的回收演算法,以此來達到高效的回收演算法。

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

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