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

垃圾回收算法與 JVM 垃圾回收器綜述概括於筆者的 JVM 內部原理與性能調優系列文章,文中涉及的引用資料參考 Java 學習與實踐資料索引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倍以上。從不一樣角度分析垃圾回收器,能夠將其分爲不一樣的類型:緩存

分類標準 描述
線程數 分爲串行垃圾回收器和並行垃圾回收器。串行垃圾回收器一次只使用一個線程進行垃圾回收;並行垃圾回收器一次將開啓多個線程同時進行垃圾回收。在並行能力較強的 CPU 上,使用並行垃圾回收器能夠縮短 GC 的停頓時間。
工做模式 分爲併發式垃圾回收器和獨佔式垃圾回收器。併發式垃圾回收器與應用程序線程交替工做,以儘量減小應用程序的停頓時間;獨佔式垃圾回收器 (Stop the world) 一旦運行,就中止應用程序中的其餘全部線程,直到垃圾回收過程徹底結束。
碎片處理方式 分爲壓縮式垃圾回收器和非壓縮式垃圾回收器。壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片;非壓縮式的垃圾回收器不進行這步操做。
工做的內存區間 新生代垃圾回收器和老年代垃圾回收器

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

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

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

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

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

  • 反應時間:指當一個對象被稱爲垃圾後多長時間內,它所佔據的內存空間會被釋放。ide

  • 堆分配:不一樣的垃圾回收器對堆內存的分配方式多是不一樣的。一個良好的垃圾回收器應該有一個合理的堆內存區間劃分。佈局

在對象查找算法的幫助下咱們能夠找到內存能夠被使用的,或者說那些內存是能夠回收,更多的時候咱們確定願意作更少的事情達到一樣的目的。

對象引用

在 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<String> sr = new SoftReference<String>(new String("hello"));
        System.out.println(sr.get());
    }
}

WeakReference: 弱引用

弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。

import java.lang.ref.WeakReference;

public class Main {
    public static void main(String[] args) {

        WeakReference<String> sr = new WeakReference<String>(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<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(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 是應該被回收的垃圾對象,但因爲垃圾對象間相互引用,從而使垃圾回收器沒法識別,引發內存泄漏。

引用樹遍歷

所謂的引用樹本質上是有根的圖結構,它沿着對象的根句柄向下查找到活着的節點,並標記下來;其他沒有被標記的節點就是死掉的節點,這些對象就是能夠被回收的,或者說活着的節點就是能夠被拷貝走的,具體要看所在 HeapSize中 的區域以及算法,它的大體示意圖以下圖所示(注意這裏是指針是單向的):

首先,全部回收器都會經過一個標記過程來對存活對象進行統計。JVM 中用到的全部現代 GC 算法在回收前都會先找出全部仍存活的對象。下圖中所展現的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;
}

通用垃圾回收算法

算法名 優點 缺陷
Mark-Sweep / 標記-清除 簡單 效率低下且會產生不少不連續內存,分配大對象時,容易提早引發另外一次垃圾回收。
Copying / 複製 效率較高,不用考慮內存碎片化 存在空間浪費
Mark-Compact / 標記-整理 避免了內存碎片化 GC 暫停時間增加

Mark-Sweep: 標記-清除算法

標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段首先經過根節點,標記全部從根節點開始的較大對象。所以,未被標記的對象就是未被引用的垃圾對象。而後,在清除階段,清除全部未被標記的對象。該算法最大的問題是存在大量的空間碎片,由於回收後的空間是不連續的。在對象的堆空間分配過程當中,尤爲是大對象的內存分配,不連續的內存空間的工做效率要低於連續的空間。

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

Copying: 複製算法

將現有的內存空間分爲兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。若是系統中的垃圾對象不少,複製算法須要複製的存活對象數量並不會太大。所以在真正須要垃圾回收的時刻,複製算法的效率是很高的。又因爲對象在垃圾回收過程當中統一被複制到新的內存空間中,所以,可確保回收後的內存空間是沒有碎片的。該算法的缺點是將系統內存摺半。

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

標記-複製算法與標記-整理算法很是相似,它們都會將全部存活對象從新進行分配。區別在於從新分配的目標地址不一樣,複製算法是爲存活對象分配了另外的內存 區域做爲它們的新家。標記複製算法的優勢在於標記階段和複製階段能夠同時進行。它的缺點是須要一塊能容納下全部存活對象的額外的內存空間。

Mark-Compact: 標記-壓縮算法

複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在年輕代常常發生,可是在老年代更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活的對象較多,複製的成本也將很高。

標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上作了一些優化。也首先須要從根節點開始對全部可達對象作一次標記,但以後,它並不簡單地 清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。以後,清理邊界外全部的空間。這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。

標記-壓縮算法修復了標記-清除算法的短板——它將全部標記的也就是存活的對象都移動到內存區域的開始位置。這種方法的缺點就是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 垃圾回收器對比

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);每一個區間能夠屬於老年代或者年輕代,而且每一個年齡代區間能夠是物理上不連續的。

名稱 做用域 算法 特性 設置
Serial Serial GC 做用於新生代,Serial Old GC 做用於老年代垃圾收集 兩者皆採用了串行回收與 "Stop-the-World",Serial 使用的是複製算法,而 Serial Old 使用的是電俄式-標記壓縮算法 基於串行回收的垃圾回收器適用於大多數對於暫停時間要求不高的 Client 模式下的 JVM 使用 -XX:+UserSerialGC 手動指定使用 Serial 回收器執行內存回收任務
Throughput/Parallel Parallel 做用於新生代,Parallel Old 做用於老年代 並行回收和 "Stop-the-World",Parallel 使用的是複製算法,Parallel Old 使用的是標記-壓縮算法 程序吞吐量優先的應用場景中,在 Server 模式下內存回收的性能較爲不錯 使用 -XX:+UseParallelGC 手動指定使用 Parallel 回收器執行內存回收任務
CMS,Concurrent-Mark-Sweep 老年代垃圾回收器,又稱做 Mostly-Concurrent 回收器 使用了標記清除算法,分爲初始標記( Initial-Mark,Stop-the-World )、併發標記( Concurrent-Mark )、再次標記( Remark,Stop-the-World )、併發清除( Concurrent-Sweep ) 併發低延遲,吞吐量較低。通過CMS收集的堆會產生空間碎片,會帶來堆內存的浪費 使用 -XX:+UseConcMarkSweepGC 來手動指定使用 CMS 回收器執行內存回收任務
G1,Garbage First 沒有采用傳統物理隔離的新生代和老年代的佈局方式,僅僅以邏輯上劃分爲新生代和老年代,選擇的將 Java 堆區劃分爲 2048 個大小相同的獨立 Region 塊 使用了標記壓縮算法 基於並行和併發、低延遲以及暫停時間更加可控的區域化分代式服務器類型的垃圾回收器 使用 -XX:UseG1GC 來手動指定使用 G1 回收器執行內存回收任務

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

  • 開始進行標記前,須要先暫停應用線程,不然若是對象圖一直在變化的話是沒法真正去遍歷它的。暫停應用線程以便 JVM 能夠盡情地收拾家務的這種狀況又被稱之爲安全點(Safe Point),這會觸發一次Stop The World(STW)暫停。觸發安全點的緣由有許多,但最多見的應該就是垃圾回收了。

  • 暫停時間的長短並不取決於堆內對象的多少也不是堆的大小,而是存活對象的多少。所以,調高堆的大小並不會影響到標記階段的時間長短。

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

Serial GC

串行回收器主要有兩個特色:第一,它僅僅使用單線程進行垃圾回收;第二,它獨佔式的垃圾回收。在串行回收器進行垃圾回收時,Java 應用程序中的線程都須要暫停,等待垃圾回收的完成,這樣給用戶體驗形成較差效果。雖然如此,串行回收器倒是一個成熟、通過長時間生產環境考驗的極爲高效的 回收器。新生代串行處理器使用複製算法,實現相對簡單,邏輯處理特別高效,且沒有線程切換的開銷。在諸如單 CPU 處理器或者較小的應用內存等硬件平臺不是特別優越的場合,它的性能表現能夠超過並行回收器和併發回收器。在 HotSpot 虛擬機中,使用-XX:+UseSerialGC 參數能夠指定使用新生代串行回收器和老年代串行回收器。當 JVM 在 Client 模式下運行時,它是默認的垃圾回收器。老年代串行回收器使用的是標記-壓縮算法。和新生代串行回收器同樣,它也是一個串行的、獨佔式的垃圾回收器。因爲老年代垃圾回收一般會使用比新生代垃圾回 收更長的時間,所以,在堆空間較大的應用程序中,一旦老年代串行回收器啓動,應用程序極可能會所以停頓幾秒甚至更長時間。雖然如此,老年代串行回收器能夠 和多種新生代回收器配合使用,同時它也能夠做爲 CMS 回收器的備用回收器。若要啓用老年代串行回收器,能夠嘗試使用如下參數:-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。

Serial GC 的工做步驟以下所示:

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 的工做步驟以下所示:

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 工做步驟以下所示:

  • 初始標記(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 的分區示例以下圖所示:

隨着 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 的工做步驟以下所示:

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

  • 併發標記(從GC Root從堆中對象進行可達性分析找存活的對象,耗時較長但能夠與用戶線程併發執行)

  • 最終標記(爲了修正併發標記期間產生變更的那一部分標記記錄,這一期間的變化記錄在Remembered Set Log 裏,而後合併到Remembered Set裏,該階段須要STW可是可並行執行)

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

相關文章
相關標籤/搜索