看完這篇文章,我不再怕面試官問「垃圾回收」了...

前言

Java 相比 C/C++ 最顯著的特色即是引入了自動垃圾回收 (下文統一用 GC 指代自動垃圾回收),它解決了 C/C++ 最使人頭疼的內存管理問題,讓程序員專一於程序自己,不用關心內存回收這些惱人的問題,這也是 Java 能大行其道的重要緣由之一,GC 真正讓程序員的生產力獲得了釋放,可是程序員很難感知到它的存在,這就比如,咱們吃完飯後在桌上放下餐盤即走,服務員會替你收拾好這些餐盤,你不會關心服務員何時來收,怎麼收。html

有人說既然 GC 已經自動咱們完成了清理,不瞭解 GC 貌似也沒啥問題。在大多數狀況下確實沒問題,不過若是涉及到一些性能調優,問題排查等,深刻地瞭解 GC 仍是必不可少的,曾經美團經過調整 JVM 相關 GC 參數讓服務響應時間 TP90,TP99都降低了10ms+,服務可用性獲得了很大的提高!因此深刻了解 GC 是成爲一名優秀 Java 程序員的必修課!java

垃圾回收分上下篇,上篇會先講垃圾回收理論,主要包括程序員

  1. GC 的幾種主要的收集方法:標記清除、標記整理、複製算法的原理與特色,各自的優劣勢
  2. 爲啥會有 Serial ,CMS, G1 等各式樣的回收器,各自的優劣勢是什麼,爲啥沒有一個統一的萬能的垃圾回收器
  3. 新生代爲啥要設置成 Eden, S0,S1 這三個區,基於什麼考慮呢
  4. 堆外內存不受 GC 控制,那該怎麼釋放呢
  5. 對象可回收,就必定會被回收嗎?
  6. 什麼是 SafePoint,什麼是 Stop The World

下篇主要講垃圾回收的實踐,主要包括web

  1. GC 日誌格式怎麼看
  2. 主要有哪些發生 OOM 的場景
  3. 發生 OOM,如何定位,經常使用的內存調試工具備哪些

本文會從如下幾方面來闡述垃圾回收算法

  1. JVM 內存區域
  2. 如何識別垃圾
    • 引用計數法
    • 可達性算法
  3. 垃圾回收主要方法
    • 標記清除法
    • 複製法
    • 標記整理法
    • 分代收集算法
  4. 垃圾回收器對比

文字比較多,不過也爲了便於讀者理解加了很多 GC 的動畫,相信看完會有很多收穫c#

JVM 內存區域

要搞懂垃圾回收的機制,咱們首先要知道垃圾回收主要回收的是哪些數據,這些數據主要在哪一塊區域,因此咱們一塊兒來看下 JVM 的內存區域數組

  • 虛擬機棧:描述的是方法執行時的內存模型,是線程私有的,生命週期與線程相同,每一個方法被執行的同時會建立棧楨(下文會看到),主要保存執行方法時的局部變量表、操做數棧、動態鏈接和方法返回地址等信息,方法執行時入棧,方法執行完出棧,出棧就至關於清空了數據,入棧出棧的時機很明確,因此這塊區域不須要進行 GC緩存

  • 本地方法棧:與虛擬機棧功能很是相似,主要區別在於虛擬機棧爲虛擬機執行 Java 方法時服務,而本地方法棧爲虛擬機執行本地方法時服務的。這塊區域也不須要進行 GC安全

  • 程序計數器:線程獨有的, 能夠把它看做是當前線程執行的字節碼的行號指示器,好比以下字節碼內容,在每一個字節碼`前面都有一個數字(行號),咱們能夠認爲它就是程序計數器存儲的內容記錄這些數字(指令地址)有啥用呢,咱們知道 Java 虛擬機的多線程是經過線程輪流切換並分配處理器的時間來完成的,在任何一個時刻,一個處理器只會執行一個線程,若是這個線程被分配的時間片執行完了(線程被掛起),處理器會切換到另一個線程執行,當下次輪到執行被掛起的線程(喚醒線程)時,怎麼知道上次執行到哪了呢,經過記錄在程序計數器中的行號指示器便可知道,因此程序計數器的主要做用是記錄線程運行時的狀態,方便線程被喚醒時能從上一次被掛起時的狀態繼續執行,須要注意的是,程序計數器是惟一一個在 Java 虛擬機規範中沒有規定任何 OOM 狀況的區域,因此這塊區域也不須要進行 GC微信

  • 本地內存:線程共享區域,Java 8 中,本地內存,也是咱們一般說的堆外內存,包含元空間和直接內存,注意到上圖中 Java 8 和 Java 8 以前的 JVM 內存區域的區別了嗎,在 Java 8 以前有個永久代的概念,實際上指的是 HotSpot 虛擬機上的永久代,它用永久代實現了 JVM 規範定義的方法區功能,主要存儲類的信息,常量,靜態變量,即時編譯器編譯後代碼等,這部分因爲是在堆中實現的,受 GC 的管理,不過因爲永久代有 -XX:MaxPermSize 的上限,因此若是動態生成類(將類信息放入永久代)或大量地執行 String.intern (將字段串放入永久代中的常量區),很容易形成 OOM,有人說能夠把永久代設置得足夠大,但很難肯定一個合適的大小,受類數量,常量數量的多少影響很大。因此在 Java 8 中就把方法區的實現移到了本地內存中的元空間中,這樣方法區就不受 JVM 的控制了,也就不會進行 GC,也所以提高了性能(發生 GC 會發生 Stop The Word,形成性能受到必定影響,後文會提到),也就不存在因爲永久代限制大小而致使的 OOM 異常了(假設總內存1G,JVM 被分配內存 100M, 理論上元空間能夠分配 2G-100M = 1.9G,空間大小足夠),也方便在元空間中統一管理。綜上所述,在 Java 8 之後這一區域也不須要進行 GC

        畫外音: 思考一個問題,堆外內存不受 GC控制,沒法經過 GC 釋放內存,那該以什麼樣的形式釋放呢,總不能只建立不釋放吧,這樣的話內存可能很快就滿了,這裏不作詳細闡述,請看文末的參考文章

  • 堆:前面幾塊數據區域都不進行 GC,那隻剩下堆了,是的,這裏是 GC 發生的區域!對象實例和數組都是在堆上分配的,GC 也主要對這兩類數據進行回收,這塊也是咱們以後重點須要分析的區域

如何識別垃圾

上一節咱們詳細講述了 JVM 的內存區域,知道了 GC 主要發生在堆,那麼 GC 該怎麼判斷堆中的對象實例或數據是否是垃圾呢,或者說判斷某些數據是不是垃圾的方法有哪些。

引用計數法

最容易想到的一種方式是引用計數法,啥叫引用計數法,簡單地說,就是對象被引用一次,在它的對象頭上加一次引用次數,若是沒有被引用(引用次數爲 0),則此對象可回收

String ref = new String("Java");

以上代碼 ref1 引用了右側定義的對象,因此引用次數是 1

若是在上述代碼後面添加一個 ref = null,則因爲對象沒被引用,引用次數置爲 0,因爲不被任何變量引用,此時即被回收,動圖以下

看起來用引用計數確實沒啥問題了,不過它沒法解決一個主要的問題:循環引用!啥叫循環引用

public  class TestRC {

    TestRC instance;
public TestRC(String name) {
    }

public static void main(String[] args) {
// 第一步
A a = new TestRC("a");
B b = new TestRC("b");

// 第二步
a.instance = b;
b.instance = a;

// 第三步
a = null;
b = null;
}
}

按步驟一步步畫圖

到了第三步,雖然 a,b 都被置爲 null 了,可是因爲以前它們指向的對象互相指向了對方(引用計數都爲 1),因此沒法回收,也正是因爲沒法解決循環引用的問題,因此現代虛擬機都不用引用計數法來判斷對象是否應該被回收。

可達性算法

現代虛擬機基本都是採用這種算法來判斷對象是否存活,可達性算法的原理是以一系列叫作  GC Root  的對象爲起點出發,引出它們指向的下一個節點,再如下個節點爲起點,引出此節點指向的下一個結點。。。(這樣經過 GC Root 串成的一條線就叫引用鏈),直到全部的結點都遍歷完畢,若是相關對象不在任意一個以 GC Root 爲起點的引用鏈中,則這些對象會被判斷爲「垃圾」,會被 GC 回收。

如圖示,若是用可達性算法便可解決上述循環引用的問題,由於從GC Root 出發沒有到達 a,b,因此 a,b 可回收

a, b 對象可回收,就必定會被回收嗎?並非,對象的 finalize 方法給了對象一次垂死掙扎的機會,當對象不可達(可回收)時,當發生GC時,會先判斷對象是否執行了 finalize 方法,若是未執行,則會先執行 finalize 方法,咱們能夠在此方法裏將當前對象與 GC Roots 關聯,這樣執行 finalize 方法以後,GC 會再次判斷對象是否可達,若是不可達,則會被回收,若是可達,則不回收!

注意: finalize 方法只會被執行一次,若是第一次執行 finalize 方法此對象變成了可達確實不會回收,但若是對象再次被 GC,則會忽略 finalize 方法,對象會被回收!這一點切記!

那麼這些 GC Roots 究竟是什麼東西呢,哪些對象能夠做爲 GC Root 呢,有如下幾類

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中 JNI(即通常說的 Native 方法)引用的對象

虛擬機棧中引用的對象

以下代碼所示,a 是棧幀中的本地變量,當 a = null 時,因爲此時 a 充當了 GC Root 的做用,a 與原來指向的實例 new Test() 斷開了鏈接,因此對象會被回收。

public class Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}

方法區中類靜態屬性引用的對象

以下代碼所示,當棧幀中的本地變量 a = null 時,因爲 a 原來指向的對象與 GC Root (變量 a) 斷開了鏈接,因此 a 原來指向的對象會被回收,而因爲咱們給 s 賦值了變量的引用,s 在此時是類靜態屬性引用,充當了 GC Root 的做用,它指向的對象依然存活!

public  class Test {
    public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}

方法區中常量引用的對象

以下代碼所示,常量 s 指向的對象並不會由於 a 指向的對象被回收而回收

public  class Test {
public static final Test s = new Test();
    public static void main(String[] args) {
    Test a = new Test();
    a = null;
    }
}

本地方法棧中 JNI 引用的對象

這是簡單給不清楚本地方法爲什麼物的童鞋簡單解釋一下:所謂本地方法就是一個 java 調用非 java 代碼的接口,該方法並不是 Java 實現的,可能由 C 或 Python等其餘語言實現的, Java 經過 JNI 來調用本地方法, 而本地方法是以庫文件的形式存放的(在 WINDOWS 平臺上是 DLL 文件形式,在 UNIX 機器上是 SO 文件形式)。經過調用本地的庫文件的內部方法,使 JAVA 能夠實現和本地機器的緊密聯繫,調用系統級的各接口方法,仍是不明白?見文末參考,對本地方法定義與使用有詳細介紹。

當調用 Java 方法時,虛擬機會建立一個棧楨並壓入 Java 棧,而當它調用的是本地方法時,虛擬機會保持 Java 棧不變,不會在 Java 棧禎中壓入新的禎,虛擬機只是簡單地動態鏈接並直接調用指定的本地方法。

JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
// 緩存String的class
jclass jc = (*env)->FindClass(env, STRING_PATH);
}

如上代碼所示,當 java 調用以上本地方法時,jc 會被本地方法棧壓入棧中, jc 就是咱們說的本地方法棧中 JNI 的對象引用,所以只會在此本地方法執行完成後纔會被釋放。

垃圾回收主要方法

上一節咱們知道了能夠經過可達性算法來識別哪些數據是垃圾,那該怎麼對這些垃圾進行回收呢。主要有如下幾種方式方式

  • 標記清除算法
  • 複製算法
  • 標記整理法

標記清除算法

步驟很簡單

  1. 先根據可達性算法 標記出相應的可回收對象(圖中黃色部分)
  2. 對可回收的對象進行回收 操做起來確實很簡單,也不用作移動數據的操做,那有啥問題呢?仔細看上圖,沒錯,內存碎片!假如咱們想在上圖中的堆中分配一塊須要 連續內存佔用 4M 或 5M 的區域,顯然是會失敗,怎麼解決呢,若是能把上面未使用的 2M, 2M,1M 內存能連起來就能連成一片可用空間爲 5M 的區域便可,怎麼作呢?

複製算法

把堆等分紅兩塊區域, A 和 B,區域 A 負責分配對象,區域 B 不分配, 對區域 A 使用以上所說的標記法把存活的對象標記出來(下圖有誤無需清除),而後把區域 A 中存活的對象都複製到區域 B(存活對象都依次緊鄰排列)最後把 A 區對象所有清理掉釋放出空間,這樣就解決了內存碎片的問題了。

不過複製算法的缺點很明顯,好比給堆分配了 500M 內存,結果只有 250M 可用,空間無緣無故減小了一半!這確定是不能接受的!另外每次回收也要把存活對象移動到另外一半,效率低下(咱們能夠想一想刪除數組元素再把非刪除的元素往一端移,效率顯然堪憂)

標記整理法

前面兩步和標記清除法同樣,不一樣的是它在標記清除法的基礎上添加了一個整理的過程 ,即將全部的存活對象都往一端移動,緊鄰排列(如圖示),再清理掉另外一端的全部區域,這樣的話就解決了內存碎片的問題。

可是缺點也很明顯:每進一次垃圾清除都要頻繁地移動存活的對象,效率十分低下。

分代收集算法

分代收集算法整合了以上算法,綜合了這些算法的優勢,最大程度避免了它們的缺點,因此是現代虛擬機採用的首選算法,與其說它是算法,倒不是說它是一種策略,由於它是把上述幾種算法整合在了一塊兒,爲啥須要分代收集呢,來看一下對象的分配有啥規律如圖示:縱軸表明已分配的字節,而橫軸表明程序運行時間

由圖可知,大部分的對象都很短命,都在很短的時間內都被回收了(IBM 專業研究代表,通常來講,98% 的對象都是朝生夕死的,通過一次 Minor GC 後就會被回收),因此分代收集算法根據對象存活週期的不一樣將堆分紅新生代和老生代(Java8之前還有個永久代),默認比例爲 1 : 2,新生代又分爲 Eden 區, from Survivor 區(簡稱S0),to Survivor 區(簡稱 S1),三者的比例爲 8: 1 : 1,這樣就能夠根據新老生代的特色選擇最合適的垃圾回收算法,咱們把新生代發生的 GC 稱爲 Young GC(也叫 Minor GC),老年代發生的 GC 稱爲 Old GC(也稱爲 Full GC)。

畫外音:思考一下,新生代爲啥要分這麼多區?

那麼分代垃圾收集是怎麼工做的呢,咱們一塊兒來看看

分代收集工做原理

一、對象在新生代的分配與回收

由以上的分析可知,大部分對象在很短的時間內都會被回收,對象通常分配在 Eden 區

當 Eden 區將滿時,觸發 Minor GC

咱們以前怎麼說來着,大部分對象在短期內都會被回收, 因此通過 Minor GC 後只有少部分對象會存活,它們會被移到 S0 區(這就是爲啥空間大小  Eden: S0: S1 = 8:1:1, Eden 區遠大於 S0,S1 的緣由,由於在 Eden 區觸發的 Minor GC 把大部對象(接近98%)都回收了,只留下少許存活的對象,此時把它們移到 S0 或 S1 綽綽有餘)同時對象年齡加一(對象的年齡即發生 Minor GC 的次數),最後把 Eden 區對象所有清理以釋放出空間,動圖以下

當觸發下一次 Minor GC 時,會把 Eden 區的存活對象和 S0(或S1) 中的存活對象(S0 或 S1 中的存活對象通過每次 Minor GC 均可能被回收)一塊兒移到 S1(Eden 和 S0 的存活對象年齡+1), 同時清空 Eden 和 S0 的空間。

若再觸發下一次 Minor GC,則重複上一步,只不過此時變成了 從 Eden,S1 區將存活對象複製到 S0 區,每次垃圾回收, S0, S1 角色互換,都是從 Eden ,S0(或S1) 將存活對象移動到 S1(或S0)。也就是說在 Eden 區的垃圾回收咱們採用的是複製算法,由於在 Eden 區分配的對象大部分在 Minor GC 後都消亡了,只剩下極少部分存活對象(這也是爲啥 Eden:S0:S1 默認爲 8:1:1 的緣由),S0,S1 區域也比較小,因此最大限度地下降了複製算法形成的對象頻繁拷貝帶來的開銷。

二、對象什麼時候晉升老年代

  • 當對象的年齡達到了咱們設定的閾值,則會從S0(或S1)晉升到老年代如圖示:年齡閾值設置爲 15, 當發生下一次 Minor GC 時,S0 中有個對象年齡達到 15,達到咱們的設定閾值,晉升到老年代!

  • 大對象 當某個對象分配須要大量的連續內存時,此時對象的建立不會分配在 Eden 區,會直接分配在老年代,由於若是把大對象分配在 Eden 區, Minor GC 後再移動到 S0,S1 會有很大的開銷(對象比較大,複製會比較慢,也佔空間),也很快會佔滿 S0,S1 區,因此乾脆就直接移到老年代.

  • 還有一種狀況也會讓對象晉升到老年代,即在 S0(或S1) 區相同年齡的對象大小之和大於 S0(或S1)空間一半以上時,則年齡大於等於該年齡的對象也會晉升到老年代。

三、空間分配擔保

在發生 MinorGC 以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間,若是大於,那麼Minor GC 能夠確保是安全的,若是不大於,那麼虛擬機會查看 HandlePromotionFailure 設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於則進行 Minor GC,不然可能進行一次 Full GC。

四、Stop The World

若是老年代滿了,會觸發 Full GC, Full GC 會同時回收新生代和老年代(即對整個堆進行GC),它會致使 Stop The World(簡稱 STW),形成挺大的性能開銷。

什麼是 STW ?所謂的 STW, 即在 GC(minor GC 或 Full GC)期間,只有垃圾回收器線程在工做,其餘工做線程則被掛起。

畫外音:爲啥在垃圾收集期間其餘工做線程會被掛起?想象一下,你一邊在收垃圾,另一羣人一邊丟垃圾,垃圾能收拾乾淨嗎。

通常 Full GC 會致使工做線程停頓時間過長(由於Full GC 會清理整個堆中的不可用對象,通常要花較長的時間),若是在此 server 收到了不少請求,則會被拒絕服務!因此咱們要儘可能減小 Full GC(Minor GC 也會形成 STW,但只會觸發輕微的 STW,由於 Eden 區的對象大部分都被回收了,只有極少數存活對象會經過複製算法轉移到 S0 或 S1 區,因此相對還好)。

如今咱們應該明白把新生代設置成 Eden, S0,S1區或者給對象設置年齡閾值或者默認把新生代與老年代的空間大小設置成 1:2 都是爲了儘量地避免對象過早地進入老年代,儘量晚地觸發 Full GC。想一想新生代若是隻設置 Eden 會發生什麼,後果就是每通過一次 Minor GC,存活對象會過早地進入老年代,那麼老年代很快就會裝滿,很快會觸發 Full GC,而對象其實在通過兩三次的 Minor GC 後大部分都會消亡,因此有了 S0,S1的緩衝,只有少數的對象會進入老年代,老年代大小也就不會這麼快地增加,也就避免了過早地觸發 Full GC。

因爲 Full GC(或Minor GC) 會影響性能,因此咱們要在一個合適的時間點發起 GC,這個時間點被稱爲 Safe Point,這個時間點的選定既不能太少以讓 GC 時間太長致使程序過長時間卡頓,也不能過於頻繁以致於過度增大運行時的負荷。通常當線程在這個時間點上狀態是能夠肯定的,如肯定 GC Root 的信息等,可使 JVM 開始安全地 GC。Safe Point 主要指的是如下特定位置:

  • 循環的末尾
  • 方法返回前
  • 調用方法的 call 以後
  • 拋出異常的位置 另外須要注意的是因爲新生代的特色(大部分對象通過 Minor GC後會消亡), Minor GC 用的是複製算法,而在老生代因爲對象比較多,佔用的空間較大,使用複製算法會有較大開銷(複製算法在對象存活率較高時要進行屢次複製操做,同時浪費一半空間)因此根據老生代特色,在老年代進行的 GC 通常採用的是標記整理法來進行回收。

垃圾收集器種類

若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。Java 虛擬機規範並無規定垃圾收集器應該如何實現,所以通常來講不一樣廠商,不一樣版本的虛擬機提供的垃圾收集器實現可能會有差異,通常會給出參數來讓用戶根據應用的特色來組合各個年代使用的收集器,主要有如下垃圾收集器

  • 在新生代工做的垃圾回收器:Serial, ParNew, ParallelScavenge
  • 在老年代工做的垃圾回收器:CMS,Serial Old, Parallel Old
  • 同時在新老生代工做的垃圾回收器:G1

圖片中的垃圾收集器若是存在連線,則表明它們之間能夠配合使用,接下來咱們來看看各個垃圾收集器的具體功能。

新生代收集器

Serial 收集器

Serial 收集器是工做在新生代的,單線程的垃圾收集器,單線程意味着它只會使用一個 CPU 或一個收集線程來完成垃圾回收,不只如此,還記得咱們上文提到的 STW 了嗎,它在進行垃圾收集時,其餘用戶線程會暫停,直到垃圾收集結束,也就是說在 GC 期間,此時的應用不可用。

看起來單線程垃圾收集器不太實用,不過咱們須要知道的任何技術的使用都不能脫離場景,在 Client 模式下,它簡單有效(與其餘收集器的單線程比),對於限定單個 CPU 的環境來講,Serial 單線程模式無需與其餘線程交互,減小了開銷,專心作 GC 能將其單線程的優點發揮到極致,另外在用戶的桌面應用場景,分配給虛擬機的內存通常不會很大,收集幾十甚至一兩百兆(僅是新生代的內存,桌面應用基本不會再大了),STW 時間能夠控制在一百多毫秒內,只要不是頻繁發生,這點停頓是能夠接受的,因此對於運行在 Client 模式下的虛擬機,Serial 收集器是新生代的默認收集器

ParNew 收集器

ParNew 收集器是 Serial 收集器的多線程版本,除了使用多線程,其餘像收集算法,STW,對象分配規則,回收策略與 Serial 收集器完成同樣,在底層上,這兩種收集器也共用了至關多的代碼,它的垃圾收集過程以下

ParNew 主要工做在 Server 模式,咱們知道服務端若是接收的請求多了,響應時間就很重要了,多線程可讓垃圾回收得更快,也就是減小了 STW 時間,能提高響應時間,因此是許多運行在 Server 模式下的虛擬機的首選新生代收集器,另外一個與性能無關的緣由是由於除了 Serial  收集器,只有它能與 CMS 收集器配合工做,CMS 是一個劃時代的垃圾收集器,是真正意義上的併發收集器,它第一次實現了垃圾收集線程與用戶線程(基本上)同時工做,它採用的是傳統的 GC 收集器代碼框架,與 Serial,ParNew 共用一套代碼框架,因此能與這二者一塊兒配合工做,然後文提到的 Parallel Scavenge 與 G1 收集器沒有使用傳統的 GC 收集器代碼框架,而是另起爐竈獨立實現的,另一些收集器則只是共用了部分的框架代碼,因此沒法與 CMS 收集器一塊兒配合工做。

在多 CPU 的狀況下,因爲 ParNew 的多線程回收特性,毫無疑問垃圾收集會更快,也能有效地減小 STW 的時間,提高應用的響應速度。

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一個使用複製算法多線程,工做於新生代的垃圾收集器,看起來功能和 ParNew 收集器同樣,它有啥特別之處嗎

關注點不一樣,CMS 等垃圾收集器關注的是儘量縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 目標是達到一個可控制的吞吐量(吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間+垃圾收集時間)),也就是說 CMS 等垃圾收集器更適合用到與用戶交互的程序,由於停頓時間越短,用戶體驗越好,而 Parallel Scavenge 收集器關注的是吞吐量,因此更適合作後臺運算等不須要太多用戶交互的任務。

Parallel Scavenge 收集器提供了兩個參數來精確控制吞吐量,分別是控制最大垃圾收集時間的 -XX:MaxGCPauseMillis 參數及直接設置吞吐量大小的 -XX:GCTimeRatio(默認99%)

除了以上兩個參數,還能夠用 Parallel Scavenge 收集器提供的第三個參數 -XX:UseAdaptiveSizePolicy,開啓這個參數後,就不須要手工指定新生代大小,Eden 與 Survivor 比例(SurvivorRatio)等細節,只須要設置好基本的堆大小(-Xmx 設置最大堆),以及最大垃圾收集時間與吞吐量大小,虛擬機就會根據當前系統運行狀況收集監控信息,動態調整這些參數以儘量地達到咱們設定的最大垃圾收集時間或吞吐量大小這兩個指標。自適應策略也是 Parallel Scavenge  與 ParNew 的重要區別!

老年代收集器

Serial Old 收集器

上文咱們知道, Serial 收集器是工做於新生代的單線程收集器,與之相對地,Serial Old 是工做於老年代的單線程收集器,此收集器的主要意義在於給 Client 模式下的虛擬機使用,若是在 Server 模式下,則它還有兩大用途:一種是在 JDK 1.5 及以前的版本中與 Parallel Scavenge 配合使用,另外一種是做爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用(後文講述),它與 Serial 收集器配合使用示意圖以下

Parallel Old 收集器

Parallel Old 是相對於 Parallel Scavenge 收集器的老年代版本,使用多線程和標記整理法,二者組合示意圖以下,這二者的組合因爲都是多線程收集器,真正實現了「吞吐量優先」的目標

CMS 收集器

CMS 收集器是以實現最短 STW 時間爲目標的收集器,若是應用很重視服務的響應速度,但願給用戶最好的體驗,則 CMS 收集器是個很不錯的選擇!

咱們以前說老年代主要用標記整理法,而 CMS 雖然工做於老年代,但採用的是標記清除法,主要有如下四個步驟

  1. 初始標記
  2. 併發標記
  3. 從新標記
  4. 併發清除

從圖中能夠的看到初始標記和從新標記兩個階段會發生 STW,形成用戶線程掛起,不過初始標記僅標記 GC Roots 能關聯的對象,速度很快,併發標記是進行 GC Roots  Tracing 的過程,從新標記是爲了修正併發標記期間因用戶線程繼續運行而致使標記產生變更的那一部分對象的標記記錄,這一階段停頓時間通常比初始標記階段稍長,但遠比並發標記時間短

整個過程當中耗時最長的是併發標記和標記清理,不過這兩個階段用戶線程均可工做,因此不影響應用的正常使用,因此整體上看,能夠認爲 CMS 收集器的內存回收過程是與用戶線程一塊兒併發執行的。

可是 CMS 收集器遠達不到完美的程度,主要有如下三個缺點

  • CMS 收集器對 CPU 資源很是敏感  緣由也能夠理解,好比原本我原本能夠有 10 個用戶線程處理請求,如今卻要分出 3 個做爲回收線程,吞吐量降低了30%,CMS 默認啓動的回收線程數是 (CPU數量+3)/ 4, 若是 CPU 數量只有一兩個,那吞吐量就直接降低 50%,顯然是不可接受的
  • CMS 沒法處理浮動垃圾(Floating Garbage),可能出現 「Concurrent Mode Failure」而致使另外一次 Full GC 的產生,因爲在併發清理階段用戶線程還在運行,因此清理的同時新的垃圾也在不斷出現,這部分垃圾只能在下一次 GC 時再清理掉(即浮雲垃圾),同時在垃圾收集階段用戶線程也要繼續運行,就須要預留足夠多的空間要確保用戶線程正常執行,這就意味着 CMS 收集器不能像其餘收集器同樣等老年代滿了再使用,JDK 1.5 默認當老年代使用了68%空間後就會被激活,固然這個比例能夠經過 -XX:CMSInitiatingOccupancyFraction 來設置,可是若是設置地過高很容易致使在 CMS 運行期間預留的內存沒法知足程序要求,會致使 Concurrent Mode Failure 失敗,這時會啓用 Serial Old 收集器來從新進行老年代的收集,而咱們知道 Serial Old 收集器是單線程收集器,這樣就會致使 STW 更長了。
  • CMS 採用的是標記清除法,上文咱們已經提到這種方法會產生大量的內存碎片,這樣會給大內存分配帶來很大的麻煩,若是沒法找到足夠大的連續空間來分配對象,將會觸發 Full GC,這會影響應用的性能。固然咱們能夠開啓 -XX:+UseCMSCompactAtFullCollection(默認是開啓的),用於在 CMS 收集器頂不住要進行 FullGC 時開啓內存碎片的合併整理過程,內存整理會致使 STW,停頓時間會變長,還能夠用另外一個參數 -XX:CMSFullGCsBeforeCompation 用來設置執行多少次不壓縮的 Full GC 後跟着帶來一次帶壓縮的。

G1(Garbage First) 收集器

G1 收集器是面向服務端的垃圾收集器,被稱爲駕馭一切的垃圾回收器,主要有如下幾個特色

  • 像 CMS 收集器同樣,能與應用程序線程併發執行。
  • 整理空閒空間更快。
  • 須要 GC 停頓時間更好預測。
  • 不會像 CMS 那樣犧牲大量的吞吐性能。
  • 不須要更大的 Java Heap


與 CMS 相比,它在如下兩個方面表現更出色
  1. 運做期間不會產生內存碎片,G1 從總體上看採用的是標記-整理法,局部(兩個 Region)上看是基於複製算法實現的,兩個算法都不會產生內存碎片,收集後提供規整的可用內存,這樣有利於程序的長時間運行。
  2. 在 STW 上創建了 可預測的停頓時間模型,用戶能夠指按期望停頓時間,G1 會將停頓時間控制在用戶設定的停頓時間之內。

爲何G1能創建可預測的停頓模型呢,主要緣由在於 G1 對堆空間的分配與傳統的垃圾收集器不一器,傳統的內存分配就像咱們前文所述,是連續的,分紅新生代,老年代,新生代又分 Eden,S0,S1,以下

而 G1 各代的存儲地址不是連續的,每一代都使用了 n 個不連續的大小相同的 Region,每一個Region佔有一塊連續的虛擬內存地址,如圖示

除了和傳統的新老生代,倖存區的空間區別,Region還多了一個H,它表明Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大於等於region一半的對象,這樣超大對象就直接分配到了老年代,防止了反覆拷貝移動。那麼 G1 分配成這樣有啥好處呢?

傳統的收集器若是發生 Full GC 是對整個堆進行全區域的垃圾收集,而分配成各個 Region 的話,方便 G1 跟蹤各個 Region 裏垃圾堆積的價值大小(回收所得到的空間大小及回收所需經驗值),這樣根據價值大小維護一個優先列表,根據容許的收集時間,優先收集回收價值最大的 Region,也就避免了整個老年代的回收,也就減小了 STW 形成的停頓時間。同時因爲只收集部分 Region,可就作到了 STW 時間的可控。

G1 收集器的工做步驟以下

  1. 初始標記
  2. 併發標記
  3. 最終標記
  4. 篩選回收

能夠看到總體過程與 CMS 收集器很是相似,篩選階段會根據各個 Region 的回收價值和成本進行排序,根據用戶指望的 GC 停頓時間來制定回收計劃。

總結

本文簡述了垃圾回收的原理與垃圾收集器的種類,相信你們對開頭提的一些問題應該有了更深入的認識,在生產環境中咱們要根據不一樣的場景來選擇垃圾收集器組合,若是是運行在桌面環境處於 Client 模式的,則用 Serial + Serial Old 收集器綽綽有餘,若是須要響應時間快,用戶體驗好的,則用 ParNew + CMS 的搭配模式,即便是號稱是「駕馭一切」的 G1,也須要根據吞吐量等要求適當調整相應的 JVM 參數,沒有最牛的技術,只有最合適的使用場景,切記!

理論有了,下一篇咱們會進入手動操做環節,咱們會一塊兒來動手操做一些 demo,作一些實驗,來驗證咱們看到的一些現象,好比對象通常分配在新生代,什麼狀況下會直接到老年代,該怎麼實驗?發生了OOM,該用哪些工具調試呢?等等,敬請期待!


參考

堆外內存的回收機制分析 https://www.jianshu.com/p/35cf0f348275 

java調用本地方法--jni簡介 https://blog.csdn.net/w1992wishes/article/details/80283403 

我們從頭至尾說一次 Java 垃圾回收 https://mp.weixin.qq.com/s/pR7U1OTwsNSg5fRyWafucA 

深刻理解 Java 虛擬機 

Java Hotspot G1 GC的一些關鍵技術 https://tech.meituan.com/2016/09/23/g1.html


    

往期推薦

千萬不要這樣寫代碼!9種常見的OOM場景演示


騰訊推出高性能 RPC 開發框架


Java中竟有18種隊列?45張圖!安排


關注我,天天陪你進步一點點!



本文分享自微信公衆號 - Java中文社羣(javacn666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索