JAVA垃圾回收器與垃圾回收演算法
1. 垃圾回收演算法
比較出名的有:
標記-清除演算法;
標記-整理演算法;
複製演算法;
分代收集演算法。
垃圾回收演算法,都首先要依賴一個基礎演算法,姑且稱為標記演算法,用於找出哪些物件可以回收。比較出名的是引用計數演算法
1.1 標記演算法
1.1.1 引用計數演算法
這是一個容易理解,又不實用的演算法。他的做法如下:
給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器減1;任何時刻計數器都為0的物件就是不可能再被使用的。
引用計數演算法的實現簡單,判斷效率也很高,在大部分情況下它都是一個不錯的演算法。但是JVM中沒有選用引用計數演算法來管理記憶體,其中最主要的一個原因是它很難解決物件之間相互迴圈引用的問題。
舉個例子:
/**
* 執行後,objA和objB會不會被GC呢?
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 這個成員屬性的唯一意義就是佔點記憶體,以便能在GC日誌中看清楚是否被回收過
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假設在這行發生了GC,objA和ojbB是否被回收
System.gc();
}
}
結果是,不會被回收,因為第一個物件,有2個引用:objA和對二個物件的instance。即使objA = null,對一個物件來說,還有instance引用在(objB = null不會導致第二個物件回收,因為它也有第一個物件的intance在引用)
1.1.2 可達性分析演算法
在主流的商用程式語言中(Java和C#),都是使用可達性分析演算法判斷物件是否存活的。這個演算法的基本思路就是通過一系列名為”GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的,下圖物件object5, object6, object7雖然有互相判斷,但它們到GC Roots是不可達的,所以它們將會判定為是可回收物件。
在Java語言裡,可作為GC Roots物件的包括如下幾種:
a.虛擬機器棧(棧楨中的本地變量表)中的引用的物件
b.方法區中的類靜態屬性引用的物件
c.方法區中的常量引用的物件
d.本地方法棧中JNI的引用的物件
1.2 回收演算法
1.2.1 標記-清除演算法Mark-Sweep
先標記出哪些物件需要回收,再做清理。沒啥突出優點,缺點是,清除後會產生大量的記憶體碎片。具體如下:
1.2.2 標記-整理演算法Mark-Compact
先標記,再把所有存活的物件向一端移動,然後直接清理端邊界意外的記憶體。
1.2.3 複製演算法
“複製”(Copying)的收集演算法,它將可用記憶體按需要分成兩塊(實際情況不一定2塊,塊的大小比例不一定),每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
複製演算法一般是使用在新生代中,因為新生代中的物件一般都是朝生夕死的,存活物件的數量並不多,這樣使用複製演算法進行拷貝時效率比較高。
突然說個新生代,可能有點懵逼,其實是2.6要說的分代回收演算法,會對記憶體區域做一個劃分:
將Heap 記憶體劃分為新生代(Young Generation)與老年代(Tenured Generation);
此時又把方法區稱為永久代Perm Generation);
新生代的目標就是儘可能快速的手機掉那些生命週期短的物件。老年代的目標是存大物件或宣告週期長的物件。
將新生代劃分為Eden(伊甸園) 與2塊Survivor Space(倖存者區,分別叫From Survivor Space 和 To Survivor Space) 。
然後在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行Copying 演算法。 不過jvm在應用複製演算法時,並不是把記憶體按照1:1來劃分的,這樣太浪費記憶體空間了。
實際情況是,按照8:1:1來劃分,每次GC,把Eden 和某一個 Survivor的存活物件,複製到另一個Survivor,然後把Eden和那個Survivor清空。這樣,意味著每次GC後,可以有90%的記憶體會被利用,只有10%浪費。
如果萬一存活物件放不下那個Survivor,會找老年代借一下,稱為分配擔保。
下圖給出記憶體的具體劃分:
具體工作流程如下:
在GC開始的前,物件只會存在於Eden區和From Survivor區,To Survivor區是空的。
緊接著進行GC,Eden區中所有存活的物件都會被複制到“To”,而在“From”區中,仍存活的物件會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設定,預設15)的物件會被移動到年老代中,沒有達到閾值的物件會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有物件移動到年老代中。
物件年齡計算:沒經過1次GC,年齡+1。
複製演算法的優缺點:
優點:避免記憶體碎片產生,實現簡單,執行高效。
缺點:1. 會浪費一部分記憶體空間; 2. 持續複製長生存期的物件則導致效率降低。
1.2.4 分代回收演算法
當前商業虛擬機器的垃圾收集器,大多支援分代收集演算法(Generational Collection),這種演算法其實沒有新的思想,無非是利用前面的演算法組合一下。
如上文所說,這種綜合演算法,把堆記憶體分為新生代和老年代。新生代用上面說的複製演算法,老年代,因物件存活率高,也沒有額外空間進行分配擔保,就必須使用“標記-清理”或“標記-整理”演算法。
2. 垃圾收集器
垃圾收集器是垃圾回收演算法的具體實現,Java虛擬機器規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同廠商、不同版本的虛擬機器所提供的垃圾收集器都可能會有很大的差別。
Sun HotSpot虛擬機器1.6版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。這些收集器以不同的組合形式配合工作來完成不同分代區的垃圾收集工作。1.7增加了G1收集器,可以說是收集器的最新研究成果。
2.1 各種收集器簡介
2.1.1 Serial收集器
用於新生代的單執行緒收集器。用複製演算法;垃圾收集的過程中會Stop The World(服務暫停)
優點:簡單高效
缺點:垃圾收集的過程中會Stop The World(服務暫停),停頓時間長。
2.1.2 Serial Old收集器
用於老年代的單執行緒收集器。用標記-整理演算法。
2.1.3 ParNew收集器
ParNew收集器其實就是Serial收集器的多執行緒版本。用於新生代,也是複製演算法。
2.1.4 Parallel Scavenge收集器
新生代收集器,複製演算法,幾乎和ParNew沒啥區別。但是,他主要追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間)。適合後臺應用等對互動相應要求不高的場景。
2.1.5 Parallel Old收集器
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先。用標記-整理演算法。
2.1.6 CMS(Concurrent Mark Sweep)收集器
高併發、低停頓,追求最短GC回收停頓時間。適合重視服務響應速度的應用(如網站),和 Parallel Old的追求目標相反。基於標記-清除演算法。
優點:併發收集、低停頓
缺點:產生大量空間碎片、併發階段會降低吞吐量
2.1.7 G1(Garbage First)收集器
G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中釋出的CMS收集器。與CMS收集器相比G1收集器有以下特點:
並行併發,充分利用硬體的多核硬體優勢,大大縮短Stop-The-World時間,使得GC時,Java程式可以繼續執行。
空間整合,G1收集器從整理看,是基於標記整理演算法,從區域性(兩個Region)看,是基於複製演算法,但無論如何,都不會產生記憶體空間碎片。分配大物件時不會因為無法找到連續空間而提前觸發下一次GC。
可預測停頓,這是G1的另一大優勢,降低停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。
分代收集,上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合。
G1收集器原理比較大,可以參考此文:深入理解 Java G1 垃圾收集器
2.2 收集器總結
參考
- 深入理解JAVA虛擬機器(周志明)
- java垃圾回收演算法之-coping複製
- 新生代Eden與兩個Survivor區的解釋