天天看點

常用垃圾收集算法——《深入了解Java虛拟機》筆記

概述

垃圾收集器(Garbage Collection, GC)的曆史要比Java久遠,且并非Java獨有,GC主要完成以下三件事情:

  1. 哪些記憶體需要回收
  2. 什麼時候回收
  3. 如何回收

對于Java記憶體運作時區域的各個部分,程式計數器、虛拟機棧、本地方法棧3個線程私有區域是随線程而生,又随線程而滅,是以這幾個區域的記憶體配置設定和回收都具備确定性,不需要考慮垃圾回收的問題。而Java堆和方法區這兩個線程共享區的記憶體是動态配置設定的,是以垃圾收集器主要關注的是這部分的記憶體,這裡也是垃圾回收的主戰場。

如何判斷對象死亡

引用計數算法

原理:給對象添加一個引用計數器,當有一個地方引用它時,計數器加1,當引用結束時,計數器減1,任何時候當引用計數為0時,則認為該對象沒有被使用,可以被回收。

引用計數算法實作簡單,效率也很高,也有很多技術中都使用該算法來管理記憶體。但Java虛拟機卻沒有使用該算法,最主要的原因它很難解決對象之間互相循環引用的問題。例如下面示例代碼:

public class ReferenceCountingGC {
    public Object instance = null;

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.gc();
    }
}
           

上述代碼中objA與objB互相引用,如果Java虛拟機采用引用計數算法,發生GC時,objA、objB将無法被回收。測試可知,上述Java代碼在GC時是可以回收記憶體的,是以可從側面證明Java虛拟機并非用的引用計數算法。

可達性分析算法

其實在Java、C#等主流語言中,使用的都是可達性分析(Reachability)算法來判定對象是否存活的。該算法基本思路是通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜尋,搜尋所走過的路徑叫做引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連時,用圖論原理來說,即GC Roots到這個對象是不可達的,則以此判斷該對象是不可用的。

可達性分析原理示意圖(來自Google I/O)

常用垃圾收集算法——《深入了解Java虛拟機》筆記

在Java語言中,可作為GC Roots的對象有以下幾種:

  1. 虛拟機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中類靜态屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中JNI引用的對象

常用的垃圾收集算法

标記-清除算法

标記-清除(Mark-Sweep)算法是最基礎的垃圾收集算法,其分為兩個階段:首先标記出所需要回收的對象,然後在标記完成後統一回收所有被标記的對象。

标記-清除算法的不足:

  1. 效率問題,标記和清除兩個過程的效率都不高
  2. 空間問題,标記清除後會産生大量不連續的記憶體碎片,空間碎片太多可能導緻以後要為大對象配置設定記憶體時,如果找不到足夠的連續記憶體而不得不觸發另一次垃圾收集動作。

标記-清除算法原理示意圖(來自《深入了解Java虛拟機》)

常用垃圾收集算法——《深入了解Java虛拟機》筆記

複制算法

複制(Copying)算法為了解決上述算法的效率問題,它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活的對象複制到另一塊上面,然後再把已使用過的記憶體空間一次清空。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。

複制算法原理示意圖(來自《深入了解Java虛拟機》)

常用垃圾收集算法——《深入了解Java虛拟機》筆記

現代商業虛拟機都采用這種收集算法來回收新生代,但按上述原理,将記憶體縮小為原來的一半使用,這種代價有點太大了。IBM研究證明,98%的對象是“朝生夕死”,是以不必按1:1來劃分記憶體空間,而是将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中一塊Survivor。當回收記憶體時,将Eden和第一塊Survivor中還存活的對象一次性地複制到第二塊Survivor中,然後清理掉Eden和第一塊Survivor空間。HotSpot虛拟機預設Eden與Survivor空間的大小比例為8:1,一旦第二塊Survivor不足以容納Eden與第一塊Survivor複制過來的存活對象時,這些對象将通過配置設定擔保機制進入老年代。

标記-整理算法

複制收集算法在對象存活率較高時就要頻繁進行複制操作,導緻效率變低,是以在老年代一般不選用這種算法。根據老年代的特點,有人提出了标記-整理(Mark-Compact)算法,标記過程跟标記-清除算法的一樣,但後續步驟不是直接将可回收對象進行清理,而是讓所有存活對象都向一端移動,然後直接清理掉端邊界以外的記憶體。

标記-整理算法原理示意圖(來自《深入了解Java虛拟機》)

常用垃圾收集算法——《深入了解Java虛拟機》筆記

分代收集算法

目前虛拟機的垃圾收集都采用“分代收集”(Generational Collection)算法。算法思想是跟根據對象存活周期的不同将記憶體劃分為幾塊,一般将Java堆劃分為新生代和老年代,這樣可以根據各個年代的特點采用最合适的垃圾收集算法。

在新生代中,每次垃圾收集時都有大量對象死亡,隻有少量存活,是以優選複制算法,這樣隻需要付出少量對象的複制成本就可以完成收集;而對于老年代,因為對象存活率高,且沒有額外的空間對它進行配置設定擔保,就更适合使用标記-清理或标記-整理算法來進行回收。