題注:阿里巴巴南京研發中心成立一年來業務與團隊迅速擴充套件,誠求前端/後端等各路英才。歡迎關注 [阿里南京技術專刊] (

https://

zhuanlan。zhihu。com/ali-

nanjing)

,也歡迎投遞簡歷,傳送簡歷到 zixiong。zzx@alibaba-inc。com, 誠邀各路大佬前來指教。

垃圾回收演算法與 JVM 垃圾回收器綜述歸納於筆者的 JVM 內部原理與效能調優系列文章,文中涉及的引用資料參考 Java 學習與實踐資料索引、JVM 資料索引。

我們常說的垃圾回收演算法可以分為兩部分:物件的查詢演算法與真正的回收方法。不同回收器的實現細節各有不同,但總的來說基本所有的回收器都會關注如下兩個方面:找出所有的存活物件以及清理掉所有的其它物件——也就是那些被認為是廢棄或無用的物件。Java 虛擬機器規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同的廠商、不同版本的虛擬機器所提供的垃圾收集器都可能會有很大差別,並且一般都會提供引數供使用者根據自己的應用特點和要求組合出各個年代所使用的收集器。其中最主流的四個垃圾回收器分別是:通常用於單 CPU 環境的 Serial GC、Throughput/Parallel GC、CMS GC、G1 GC。

當我們在討論垃圾回收器時,往往也會涉及到很多的概念;譬如並行(Parallel)與併發(Concurrent)、Minor GC 與 Major / Full GC。並行指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態;併發指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),使用者程式在繼續執行,而垃圾收集程式運行於另一個CPU上。Minor GC 指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快;Major GC 指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程),Major GC的速度一般會比Minor GC慢10倍以上。從不同角度分析垃圾回收器,可以將其分為不同的型別:

垃圾回收演算法與 JVM 垃圾回收器綜述

我們最常用的評價垃圾回收器的指標就是吞吐量與停頓時間,停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者的體驗;而高吞吐量則可以最高效率地利用 CPU 時間,儘快地完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務;具體的指標列舉如下:

吞吐量:指在應用程式的生命週期內,應用程式所花費的時間和系統總執行時間的比值。系統總執行時間=應用程式耗時+GC 耗時。如果系統運行了 100min,GC 耗時 1min,那麼系統的吞吐量就是 (100-1)/100=99%。

垃圾回收器負載:和吞吐量相反,垃圾回收器負載指來記回收器耗時與系統執行總時間的比值。

停頓時間:指垃圾回收器正在執行時,應用程式的暫停時間。對於獨佔回收器而言,停頓時間可能會比較長。使用併發的回收器時,由於垃圾回收器和應用程式交替執行,程式的停頓時間會變短,但是,由於其效率很可能不如獨佔垃圾回收器,故系統的吞吐量可能會較低。

垃圾回收頻率:指垃圾回收器多長時間會執行一次。一般來說,對於固定的應用而言,垃圾回收器的頻率應該是越低越好。通常增大堆空間可以有效降低垃圾回收發生的頻率,但是可能會增加回收產生的停頓時間。

反應時間:指當一個物件被稱為垃圾後多長時間內,它所佔據的記憶體空間會被釋放。

堆分配:不同的垃圾回收器對堆記憶體的分配方式可能是不同的。一個良好的垃圾回收器應該有一個合理的堆記憶體區間劃分。

在物件查詢演算法的幫助下我們可以找到記憶體可以被使用的,或者說那些記憶體是可以回收,更多的時候我們肯定願意做更少的事情達到同樣的目的。

物件引用

在 JDK 1。2 以前的版本中,若一個物件不被任何變數引用,那麼程式就無法再使用這個物件。也就是說,只有物件處於可觸及(Reachable)狀態,程式才能使用它。從 JDK 1。2 版本開始,把物件的引用分為 4 種級別,從而使程式能更加靈活地控制物件的生命週期。這 4 種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。

StrongReference: 強引用

強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java 虛擬機器寧願丟擲 OutOfMemoryError 錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。比如下面這段程式碼:

public class Main {

public static void main(String[] args) {

new Main()。fun1();

}

public void fun1() {

Object object = new Object();

Object[] objArr = new Object[1000];

}

}

當執行至 Object[] objArr = new Object[1000]; 這句時,如果記憶體不足,JVM 會丟擲 OOM 錯誤也不會回收 object 指向的物件。不過要注意的是,當 fun1 執行完之後,object 和 objArr 都已經不存在了,所以它們指向的物件都會被 JVM 回收。如果想中斷強引用和某個物件之間的關聯,可以顯示地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該物件。比如 Vector 類的 clear 方法中就是透過將引用賦值為 null 來實現清理工作的:

/**

* Removes the element at the specified position in this Vector。

* Shifts any subsequent elements to the left (subtracts one from their

* indices)。 Returns the element that was removed from the Vector。

*

* @throws ArrayIndexOutOfBoundsException if the index is out of range

* ({@code index < 0 || index >= size()})

* @param index the index of the element to be removed

* @return element that was removed

* @since 1。2

*/

public synchronized E remove(int index) {

modCount++;

if (index >= elementCount)

throw new ArrayIndexOutOfBoundsException(index);

Object oldValue = elementData[index];

int numMoved = elementCount - index - 1;

if (numMoved > 0)

System。arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[——elementCount] = null; // Let gc do its work

return (E)oldValue;

}

SoftReference: 軟引用

軟引用是用來描述一些有用但並不是必需的物件,在 Java 中用 java。lang。ref。SoftReference 類來表示。對於軟引用關聯著的物件,只有在記憶體不足的時候 JVM 才會回收該物件。因此,這一點可以很好地用來解決 OOM 的問題,並且這個特性很適合用來實現快取:比如網頁快取、圖片快取等。軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被JVM回收,這個軟引用就會被加入到與之關聯的引用佇列中。下面是一個使用示例:

import java。lang。ref。SoftReference;

public class Main {

public static void main(String[] args) {

SoftReference sr = new SoftReference(new String(“hello”));

System。out。println(sr。get());

}

}

WeakReference: 弱引用

弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

import java。lang。ref。WeakReference;

public class Main {

public static void main(String[] args) {

WeakReference sr = new WeakReference(new String(“hello”));

System。out。println(sr。get());

System。gc(); //通知JVM的gc進行垃圾回收

System。out。println(sr。get());

}

}

輸出結果為:

hello

null

第二個輸出結果是 null,這說明只要 JVM 進行垃圾回收,被弱引用關聯的物件必定會被回收掉。不過要注意的是,這裡所說的被弱引用關聯的物件是指只有弱引用與之關聯,如果存在強引用同時與之關聯,則進行垃圾回收時也不會回收該物件(軟引用也是如此)。弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。

PhantomReference: 虛引用

“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用和前面的軟引用、弱引用不同,它並不影響物件的生命週期。在 Java 中用 java。lang。ref。PhantomReference 類表示。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。要注意的是,虛引用必須和引用佇列關聯使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用佇列中。程式可以透過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。

import java。lang。ref。PhantomReference;

import java。lang。ref。ReferenceQueue;

public class Main {

public static void main(String[] args) {

ReferenceQueue queue = new ReferenceQueue();

PhantomReference pr = new PhantomReference(new String(“hello”), queue);

System。out。println(pr。get());

}

}

物件存活性判斷

常用的物件存活性判斷方法有引用計數法與可達性分析,不過由於引用計數法無法解決物件迴圈引用的問題,因此主流的 JVM 傾向於使用可達性分析。

Reference Counting: 引用計數

引用計數器在微軟的 COM 元件技術中、Adobe 的 ActionScript3 種都有使用。引用計數器的原理很簡單,對於一個物件 A,只要有任何一個物件引用了 A,則 A 的引用計數器就加 1,當引用失效時,引用計數器就減 1。只要物件 A 的引用計數器的值為 0,則物件 A 就不可能再被使用。引用計數器的實現也非常簡單,只需要為每個物件配置一個整形的計數器即可。但是引用計數器有一個嚴重的問題,即無法處理迴圈引用的情況。因此,在 Java 的垃圾回收器中沒有使用這種演算法。一個簡單的迴圈引用問題描述如下:有物件 A 和物件 B,物件 A 中含有物件 B 的引用,物件 B 中含有物件 A 的引用。此時,物件 A 和物件 B 的引用計數器都不為 0。但是在系統中卻不存在任何第 3 個物件引用了 A 或 B。也就是說,A 和 B 是應該被回收的垃圾物件,但由於垃圾物件間相互引用,從而使垃圾回收器無法識別,引起記憶體洩漏。

垃圾回收演算法與 JVM 垃圾回收器綜述

引用樹遍歷

所謂的引用樹本質上是有根的圖結構,它沿著物件的根控制代碼向下查詢到活著的節點,並標記下來;其餘沒有被標記的節點就是死掉的節點,這些物件就是可以被回收的,或者說活著的節點就是可以被複製走的,具體要看所在 HeapSize中 的區域以及演算法,它的大致示意圖如下圖所示(注意這裡是指標是單向的):

垃圾回收演算法與 JVM 垃圾回收器綜述

首先,所有回收器都會透過一個標記過程來對存活物件進行統計。JVM 中用到的所有現代 GC 演算法在回收前都會先找出所有仍存活的物件。下圖中所展示的JVM中的記憶體佈局可以用來很好地闡釋這一概念:

垃圾回收演算法與 JVM 垃圾回收器綜述

而所謂的GC根物件包括:當前執行方法中的所有本地變數及入參、活躍執行緒、已載入類中的靜態變數、JNI 引用。接下來,垃圾回收器會對記憶體中的整個物件圖進行遍歷,它先從 GC 根物件開始,然後是根物件引用的其它物件,比如例項變數。回收器將訪問到的所有物件都標記為存活。存活物件在上圖中被標記為藍色。當標記階段完成了之後,所有的存活物件都已經被標記完了。其它的那些(上圖中灰色的那些)也就是GC根物件不可達的物件,也就是說你的應用不會再用到它們了。這些就是垃圾物件,回收器將會在接下來的階段中清除它們。

不過那些發現不能到達 GC Roots 的物件並不會立即回收,在真正回收之前,物件至少要被標記兩次。當第一次被發現不可達時,該物件會被標記一次,同時呼叫此物件的 finalize()方法(如果有);在第二次被發現不可達後,物件被回收。利用 finalisze()方法,物件可以逃離一次被回收的命運,但是隻有一次。逃命方法如下,需要在 finalize() 方法中給自己加一個 GCRoots 中的 hook:

public class EscapeFromGC(){

public static EscapeFromGC hook;

@Override

protected void finalize() throws Throwable {

super。finalize();

System。out。println(“finalize mehtod executed!”);

EscapeFromGC。hook = this;

}

通用垃圾回收演算法

垃圾回收演算法與 JVM 垃圾回收器綜述

Mark-Sweep: 標記-清除演算法

垃圾回收演算法與 JVM 垃圾回收器綜述

標記-清除演算法將垃圾回收分為兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段首先透過根節點,標記所有從根節點開始的較大物件。因此,未被標記的物件就是未被引用的垃圾物件。然後,在清除階段,清除所有未被標記的物件。該演算法最大的問題是存在大量的空間碎片,因為回收後的空間是不連續的。在物件的堆空間分配過程中,尤其是大物件的記憶體分配,不連續的記憶體空間的工作效率要低於連續的空間。

從概念上來講,標記-清除演算法使用的方法是最簡單的,只需要忽略這些物件便可以了。也就是說當標記階段完成之後,未被訪問到的物件所在的空間都會被認為是空閒的,可以用來建立新的物件。這種方法需要使用一個空閒列表來記錄所有的空閒區域以及大小。對空閒列表的管理會增加分配物件時的工作量。這種方法還有一個缺陷就是——雖然空閒區域的大小是足夠的,但卻可能沒有一個單一區域能夠滿足這次分配所需的大小,因此本次分配還是會失敗(在Java中就是一次 OutOfMemoryError)。

Copying: 複製演算法

垃圾回收演算法與 JVM 垃圾回收器綜述

將現有的記憶體空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的記憶體中的存活物件複製到未被使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。如果系統中的垃圾物件很多,複製演算法需要複製的存活物件數量並不會太大。因此在真正需要垃圾回收的時刻,複製演算法的效率是很高的。又由於物件在垃圾回收過程中統一被複制到新的記憶體空間中,因此,可確保回收後的記憶體空間是沒有碎片的。該演算法的缺點是將系統內存摺半。

Java 的新生代序列垃圾回收器中使用了複製演算法的思想。新生代分為 eden 空間、from 空間、to 空間 3 個部分。其中 from 空間和 to 空間可以視為用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱為 survivor 空間,即倖存者空間,用於存放未被回收的物件。在垃圾回收時,eden 空間中的存活物件會被複制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕物件也會被複制到 to 空間中 (大物件,或者老年物件會直接進入老年帶,如果 to 空間已滿,則物件也會直接進入老年代)。此時,eden 空間和 from 空間中的剩餘物件就是垃圾物件,可以直接清空,to 空間則存放此次回收後的存活物件。這種改進的複製演算法既保證了空間的連續性,又避免了大量的記憶體空間浪費。

標記-複製演算法與標記-整理演算法非常類似,它們都會將所有存活物件重新進行分配。區別在於重新分配的目標地址不同,複製演算法是為存活物件分配了另外的記憶體 區域作為它們的新家。標記複製演算法的優點在於標記階段和複製階段可以同時進行。它的缺點是需要一塊能容納下所有存活物件的額外的記憶體空間。

Mark-Compact: 標記-壓縮演算法

垃圾回收演算法與 JVM 垃圾回收器綜述

複製演算法的高效性是建立在存活物件少、垃圾物件多的前提下的。這種情況在年輕代經常發生,但是在老年代更常見的情況是大部分物件都是存活物件。如果依然使用複製演算法,由於存活的物件較多,複製的成本也將很高。

標記-壓縮演算法是一種老年代的回收演算法,它在標記-清除演算法的基礎上做了一些最佳化。也首先需要從根節點開始對所有可達物件做一次標記,但之後,它並不簡單地 清理未標記的物件,而是將所有的存活物件壓縮到記憶體的一端。之後,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,因此,其價效比比較高。

標記-壓縮演算法修復了標記-清除演算法的短板——它將所有標記的也就是存活的物件都移動到記憶體區域的開始位置。這種方法的缺點就是GC暫停的時間會增 長,因為你需要將所有的物件都複製到一個新的地方,還得更新它們的引用地址。相對於標記-清除演算法,它的優點也是顯而易見的——經過整理之後,新物件的分 配只需要透過指標碰撞便能完成(pointer bumping),相當簡單。使用這種方法空閒區域的位置是始終可知的,也不會再有碎片的問題了。

Incremental Collecting: 增量回收演算法

在垃圾回收過程中,應用軟體將處於一種 CPU 消耗很高的狀態。在這種 CPU 消耗很高的狀態下,應用程式所有的執行緒都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程式會被掛起很久,將嚴重影響使用者體驗或者系統的穩定性。

增量演算法現代垃圾回收的一個前身,其基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集執行緒和應用程式執行緒交替執行。每次,垃圾收集執行緒只收集一小片區域的記憶體空間,接著切換到應用程式執行緒。依次反覆,直到垃圾收集完成。使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程式程式碼,所以能減少系統的停頓時間。但是,因為執行緒切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。

Generational Collecting: 分代回收演算法

分代回收器是增量收集的另一個化身,根據垃圾回收物件的特性,不同階段最優的方式是使用合適的演算法用於本階段的垃圾回收,分代演算法即是基於這種思想,它將記憶體區間根據物件的特點分成幾塊,根據 每塊記憶體區間的特點,使用不同的回收演算法,以提高垃圾回收的效率。以 Hot Spot 虛擬機器為例,它將所有的新建物件都放入稱為年輕代的記憶體區域,年輕代的特點是物件會很快回收,因此,在年輕代就選擇效率較高的複製演算法。當一個物件經過幾 次回收後依然存活,物件就會被放入稱為老生代的記憶體空間。在老生代中,幾乎所有的物件都是經過幾次垃圾回收後依然得以倖存的。因此,可以認為這些物件在一 段時期內,甚至在應用程式的整個生命週期中,將是常駐記憶體的。如果依然使用複製演算法回收老生代,將需要複製大量物件。再加上老生代的回收價效比也要低於新 生代,因此這種做法也是不可取的。根據分代的思想,可以對老年代的回收使用與新生代不同的標記-壓縮演算法,以提高垃圾回收效率。

Concurrent Collecting: 併發回收演算法

所謂的併發回收演算法即是指垃圾回收器與應用程式能夠交替工作,併發回收 器其實也會暫停,但是時間非常短,它並不會在從開始回收尋找、標記、清楚、壓縮或複製等方式過程完全暫停服務,它發現有幾個時間比較長,一個就是標記,因 為這個回收一般面對的是老年代,這個區域一般很大,而一般來說絕大部分物件應該是活著的,所以標記時間很長,還有一個時間是壓縮,但是壓縮並不一定非要每 一次做完GC都去壓縮的,而複製呢一般不會用在老年代,所以暫時不考慮;所以他們想出來的辦法就是:第一次短暫停機是將所有物件的根指標找到,這個非常容 易找到,而且非常快速,找到後,此時GC開始從這些根節點標記活著的節點(這裡可以採用並行),然後待標記完成後,此時可能有新的 記憶體申請以及被拋棄(java本身沒有記憶體釋放這一概念),此時JVM會記錄下這個過程中的增量資訊,而對於老年代來說,必須要經過多次在 survivor倒騰後才會進入老年代,所以它在這段時間增量一般來說會非常少,而且它被釋放的機率前面也說並不大(JVM如果不是完全做Cache,自 己做pageCache而且發生機率不大不小的pageout和pagein是不適合的);JVM根據這些增量資訊快速標記出內部的節點,也是非常快速 的,就可以開始回收了,由於需要殺掉的節點並不多,所以這個過程也非常快,壓縮在一定時間後會專門做一次操作,有關暫停時間在Hotspot版本,也就是 SUN的jdk中都是可以配置的,當在指定時間範圍內無法回收時,JVM將會對相應尺寸進行調整,如果你不想讓它調整,在設定各個區域的大小時,就使用定 量,而不要使用比例來控制;當採用併發回收演算法的時候,一般對於老年代區域,不會等待記憶體小於10%左右的時候才會發起回收,因為併發回收是允許在回收的 時候被分配,那樣就有可能來不及了,所以併發回收的時候,JVM可能會在68%左右的時候就開始啟動對老年代GC了。

JVM 垃圾回收器對比

垃圾回收演算法與 JVM 垃圾回收器綜述

1999 年隨 JDK1。3。1 一起來的是序列方式的 Serial GC,它是第一款垃圾回收器;此後,JDK1。4 和 J2SE1。3 相繼釋出。2002 年 2 月 26 日,J2SE1。4 釋出;Parallel GC 和Concurrent Mark Sweep (CMS)GC 跟隨 JDK1。4。2 一起釋出,並且 Parallel GC 在 JDK6 之後成為 HotSpot 預設 GC。這三個垃圾回收器也是各有千秋,Serial GC 適合最小化地使用記憶體和並行開銷的場景、Parallel GC 適合最大化應用程式吞吐量的場景、CMS GC 適合最小化中斷或停頓時間的場景。上圖即展示了多種垃圾回收器之間的關係;不過隨著應用程式所應對的業務越來越龐大、複雜,使用者越來越多,沒有合適的回收器就不能保證應用程式正常進行,而經常造成 STW 停頓的回收器又跟不上實際的需求,所以才會不斷地嘗試對蒐集器進行最佳化。Garbage First(G1)GC 正是面向這種業務需求所生,它是一個並行回收器,把堆記憶體分割為很多不相關的區間(Region);每個區間可以屬於老年代或者年輕代,並且每個年齡代區間可以是物理上不連續的。

垃圾回收演算法與 JVM 垃圾回收器綜述

關於標記階段有幾個關鍵點是值得注意的:

開始進行標記前,需要先暫停應用執行緒,否則如果物件圖一直在變化的話是無法真正去遍歷它的。暫停應用執行緒以便 JVM 可以盡情地收拾家務的這種情況又被稱之為安全點(Safe Point),這會觸發一次Stop The World(STW)暫停。觸發安全點的原因有許多,但最常見的應該就是垃圾回收了。

暫停時間的長短並不取決於堆內物件的多少也不是堆的大小,而是存活物件的多少。因此,調高堆的大小並不會影響到標記階段的時間長短。

當標記階段完成後,GC開始進入下一階段,刪除不可達物件。

Serial GC

序列回收器主要有兩個特點:第一,它僅僅使用單執行緒進行垃圾回收;第二,它獨佔式的垃圾回收。在序列回收器進行垃圾回收時,Java 應用程式中的執行緒都需要暫停,等待垃圾回收的完成,這樣給使用者體驗造成較差效果。雖然如此,序列回收器卻是一個成熟、經過長時間生產環境考驗的極為高效的 回收器。新生代序列處理器使用複製演算法,實現相對簡單,邏輯處理特別高效,且沒有執行緒切換的開銷。在諸如單 CPU 處理器或者較小的應用記憶體等硬體平臺不是特別優越的場合,它的效能表現可以超過並行回收器和併發回收器。在 HotSpot 虛擬機器中,使用-XX:+UseSerialGC 引數可以指定使用新生代序列回收器和老年代序列回收器。當 JVM 在 Client 模式下執行時,它是預設的垃圾回收器。老年代序列回收器使用的是標記-壓縮演算法。和新生代序列回收器一樣,它也是一個序列的、獨佔式的垃圾回收器。由於老年代垃圾回收通常會使用比新生代垃圾回 收更長的時間,因此,在堆空間較大的應用程式中,一旦老年代序列回收器啟動,應用程式很可能會因此停頓幾秒甚至更長時間。雖然如此,老年代序列回收器可以 和多種新生代回收器配合使用,同時它也可以作為 CMS 回收器的備用回收器。若要啟用老年代序列回收器,可以嘗試使用以下引數:-XX:+UseSerialGC: 新生代、老年代都使用序列回收器。

Serial GC 的工作步驟如下所示:

垃圾回收演算法與 JVM 垃圾回收器綜述

ParNew GC

並行回收器是工作在新生代的垃圾回收器,它只簡單地將序列回收器多執行緒化。它的回收策略、演算法以及引數和序列回收器一樣。

並行回收器 也是獨佔式的回收器,在收集過程中,應用程式會全部暫停。但由於並行回收器使用多執行緒進行垃圾回收,因此,在併發能力比較強的 CPU 上,它產生的停頓時間要短於序列回收器,而在單 CPU 或者併發能力較弱的系統中,並行回收器的效果不會比序列回收器好,由於多執行緒的壓力,它的實際表現很可能比序列回收器差。開啟並行回收器可以使用引數-XX:+UseParNewGC,該引數設定新生代使用並行回收器,老年代使用序列回收器。老年代的並行回收回收器也是一種多執行緒併發的回收器。和新生代並行回收回收器一樣,它也是一種關注吞吐量的回收器。老年代並行回收回收器使用標記-壓縮演算法,JDK1。6 之後開始啟用。

Parallel GC

Parallel Scavenge 收集器的特點是它的關注點與其他收集器不同,CMS 等收集器的關注點儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput)。Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。這個收集器是在JDK 1。6中才開始提供的。使用 -XX:+UseParallelOldGC 可以在新生代和老生代都使用並行回收回收器,這是一對非常關注吞吐量的垃圾回收器組合,在對吞吐量敏感的系統中,可以考慮使用。引數 -XX:ParallelGCThreads 也可以用於設定垃圾回收時的執行緒數量。

Parallel GC 的工作步驟如下所示:

垃圾回收演算法與 JVM 垃圾回收器綜述

CMS GC

CMS( Concurrent Mark-Sweep ) 是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器,適用於對停頓比較敏感,並且有相對較多存活時間較長的物件(老年代較大)的應用程式;不過 CMS 雖然減少了回收的停頓時間,但是降低了堆空間的利用率。CMS GC 採用了 Mark-Sweep 演算法,因此經過CMS收集的堆會產生空間碎片;為了解決堆空間浪費問題,CMS回收器不再採用簡單的指標指向一塊可用堆空間來為下次物件分配使用。而是把一些未分配的空間彙總成一個列表,當 JVM 分配物件空間的時候,會搜尋這個列表找到足夠大的空間來存放住這個物件。另一方面,由於 CMS 執行緒和應用程式執行緒併發執行,CMS GC 需要更多的 CPU 資源。同時,因為CMS標記階段應用程式的執行緒還是在執行的,那麼就會有堆空間繼續分配的情況,為了保證在CMS回收完堆之前還有空間分配給正在執行的應用程式,必須預留一部分空間。也就是說,CMS不會在老年代滿的時候才開始收集。相反,它會嘗試更早的開始收集,已避免上面提到的情況:在回收完成之前,堆沒有足夠空間分配!預設當老年代使用68%的時候,CMS就開始行動了。 – XX:CMSInitiatingOccupancyFraction =n 來設定這個閥值。

CMS GC 工作步驟如下所示:

垃圾回收演算法與 JVM 垃圾回收器綜述

初始標記(STW initial mark):在這個階段,需要虛擬機器停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的“根物件”開始,只掃描到能夠和“根物件”直接關聯的物件,並作標記。所以這個過程雖然暫停了整個JVM,但是很快就完成了。

併發標記(Concurrent marking):這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程式的執行緒和併發標記的執行緒併發執行,所以使用者不會感受到停頓。

併發預清理(Concurrent precleaning):併發預清理階段仍然是併發的。在這個階段,虛擬機器查詢在執行併發標記階段新進入老年代的物件(可能會有一些物件從新生代晉升到老年代,或者有一些物件被分配到老年代)。透過重新掃描,減少下一個階段“重新標記”的工作,因為下一個階段會Stop The World。

重新標記(STW remark):這個階段會暫停虛擬機器,回收器執行緒掃描在CMS堆中剩餘的物件。掃描從“跟物件”開始向下追溯,並處理物件關聯。

併發清理(Concurrent sweeping):清理垃圾物件,這個階段回收器執行緒和應用程式執行緒併發執行。

併發重置(Concurrent reset):這個階段,重置CMS回收器的資料結構,等待下一次垃圾回收。

G1 GC

G1 GC 是 JDK 1。7 中正式投入使用的用於取代 CMS 的壓縮回收器,它雖然沒有在物理上隔斷新生代與老生代,但是仍然屬於分代垃圾回收器;G1 GC 仍然會區分年輕代與老年代,年輕代依然分有 Eden 區與 Survivor 區。G1 GC 首先將堆分為大小相等的 Region,避免全區域的垃圾收集,然後追蹤每個 Region 垃圾堆積的價值大小,在後臺維護一個優先列表,根據允許的收集時間優先回收價值最大的Region;同時 G1 GC 採用 Remembered Set 來存放 Region 之間的物件引用以及其他回收器中的新生代與老年代之間的物件引用,從而避免全堆掃描。G1 GC 的分割槽示例如下圖所示:

垃圾回收演算法與 JVM 垃圾回收器綜述

隨著 G1 GC 的出現,Java 垃圾回收器透過引入 Region 的概念,從傳統的連續堆記憶體佈局設計,逐步走向了物理上不連續但是邏輯上依舊連續的記憶體塊;這樣我們能夠將某個 Region 動態地分配給 Eden、Survivor、老年代、大物件空間、空閒區間等任意一個。每個 Region 都有一個關聯的 Remembered Set(簡稱RS),RS 的資料結構是 Hash 表,裡面的資料是 Card Table (堆中每 512byte 對映在 card table 1byte)。簡單的說RS裡面存在的是Region中存活物件的指標。當Region中資料發生變化時,首先反映到Card Table中的一個或多個Card上,RS透過掃描內部的Card Table得知Region中記憶體使用情況和存活物件。在使用Region過程中,如果Region被填滿了,分配記憶體的執行緒會重新選擇一個新的Region,空閒Region被組織到一個基於連結串列的資料結構(LinkedList)裡面,這樣可以快速找到新的Region。

總結而言,G1 GC 的特性如下:

並行性:G1在回收期間,可以有多個GC執行緒同時工作,有效利用多核計算能力;

併發性:G1擁有與應用程式交替執行的能力,部分工作可以和應用程式同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程式的情況;

分代GC:G1依然是一個分代回收器,但是和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;

空間整理:G1在回收過程中,會進行適當的物件移動,不像CMS只是簡單地標記清理物件。在若干次GC後,CMS必須進行一次碎片整理。而G1不同,它每次回收都會有效地複製物件,減少空間碎片,進而提升內部迴圈速度;

可預見性:為了縮短停頓時間,G1建立可預存停頓的模型,這樣在使用者設定的停頓時間範圍內,G1會選擇適當的區域進行收集,確保停頓時間不超過使用者指定時間。

G1 GC 的工作步驟如下所示:

垃圾回收演算法與 JVM 垃圾回收器綜述

初始標記(標記一下GC Roots能直接關聯的物件並修改TAMS值,需要STW但耗時很短)

併發標記(從GC Root從堆中物件進行可達性分析找存活的物件,耗時較長但可以與使用者執行緒併發執行)

最終標記(為了修正併發標記期間產生變動的那一部分標記記錄,這一期間的變化記錄在Remembered Set Log 裡,然後合併到Remembered Set裡,該階段需要STW但是可並行執行)

篩選回收(對各個Region回收價值排序,根據使用者期望的GC停頓時間制定回收計劃來回收)