JVM 垃圾回收

JVM 垃圾回收

寫在前面

本節(jié)常見面試題

問題答案在文中都有提到

  • 如何判斷對象是否死亡(兩種方法)。
  • 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區(qū)別、使用軟引用能帶來的好處)。
  • 如何判斷一個常量是廢棄常量
  • 如何判斷一個類是無用的類
  • 垃圾收集有哪些算法,各自的特點?
  • HotSpot 為什么要分為新生代和老年代?
  • 常見的垃圾回收器有哪些?
  • 介紹一下 CMS,G1 收集器。
  • Minor Gc 和 Full GC 有什么不同呢?

本文導火索

當需要排查各種內(nèi)存溢出問題、當垃圾收集成為系統(tǒng)達到更高并發(fā)的瓶頸時,我們就需要對這些“自動化”的技術(shù)實施必要的監(jiān)控和調(diào)節(jié)。

1 揭開 JVM 內(nèi)存分配與回收的神秘面紗

Java 的自動內(nèi)存管理主要是針對對象內(nèi)存的回收和對象內(nèi)存的分配。同時,Java 自動內(nèi)存管理最核心的功能是 內(nèi)存中對象的分配與回收。

Java 堆是垃圾收集器管理的主要區(qū)域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由于現(xiàn)在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內(nèi)存,或者更快地分配內(nèi)存。

堆空間的基本結(jié)構(gòu):

上圖所示的 Eden 區(qū)、From Survivor0("From") 區(qū)、To Survivor1("To") 區(qū)都屬于新生代,Old Memory 區(qū)屬于老年代。

大部分情況,對象都會首先在 Eden 區(qū)域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden 區(qū)->Survivor 區(qū)后對象的初始年齡變?yōu)?1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置。

修正(issue552):“Hotspot遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了survivor區(qū)的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值”。

動態(tài)年齡計算的代碼如下

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  //survivor_capacity是survivor空間的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
 total += sizes[age];//sizes數(shù)組是每個年齡段對象大小
 if (total > desired_survivor_size) break;
 age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
  ...
}

經(jīng)過這次GC后,Eden區(qū)和"From"區(qū)已經(jīng)被清空。這個時候,"From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎樣,都會保證名為To的Survivor區(qū)域是空的。Minor GC會一直重復這樣的過程,直到“To”區(qū)被填滿,"To"區(qū)被填滿之后,會將所有對象移動到老年代中。


1.1 對象優(yōu)先在 eden 區(qū)分配

目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內(nèi)存分為新生代和老年代,這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法。

大多數(shù)情況下,對象在新生代中 eden 區(qū)分配。當 eden 區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次 Minor GC.下面我們來進行實際測試以下。

測試:

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2;
        allocation1 = new byte[30900*1024];
        //allocation2 = new byte[900*1024];
    }
}

通過以下方式運行:


添加的參數(shù):-XX:+PrintGCDetails

運行結(jié)果 (紅色字體描述有誤,應(yīng)該是對應(yīng)于 JDK1.7 的永久代):


從上圖我們可以看出 eden 區(qū)內(nèi)存幾乎已經(jīng)被分配完全(即使程序什么也不做,新生代也會使用 2000 多 k 內(nèi)存)。假如我們再為 allocation2 分配內(nèi)存會出現(xiàn)什么情況呢?

allocation2 = new byte[900*1024];

簡單解釋一下為什么會出現(xiàn)這種情況: 因為給 allocation2 分配內(nèi)存的時候 eden 區(qū)內(nèi)存幾乎已經(jīng)被分配完了,我們剛剛講了當 Eden 區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次 Minor GC.GC 期間虛擬機又發(fā)現(xiàn) allocation1 無法存入 Survivor 空間,所以只好通過 分配擔保機制 把新生代的對象提前轉(zhuǎn)移到老年代中去,老年代上的空間足夠存放 allocation1,所以不會出現(xiàn) Full GC。執(zhí)行 Minor GC 后,后面分配的對象如果能夠存在 eden 區(qū)的話,還是會在 eden 區(qū)分配內(nèi)存。可以執(zhí)行如下代碼驗證:

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
        allocation1 = new byte[32000*1024];
        allocation2 = new byte[1000*1024];
        allocation3 = new byte[1000*1024];
        allocation4 = new byte[1000*1024];
        allocation5 = new byte[1000*1024];
    }
}

1.2 大對象直接進入老年代

大對象就是需要大量連續(xù)內(nèi)存空間的對象(比如:字符串、數(shù)組)。

為什么要這樣呢?

為了避免為大對象分配內(nèi)存時由于分配擔保機制帶來的復制而降低效率。

1.3 長期存活的對象將進入老年代

既然虛擬機采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回收時就必須能識別哪些對象應(yīng)放在新生代,哪些對象應(yīng)放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數(shù)器。

如果對象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設(shè)為 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置。

1.4 動態(tài)對象年齡判定

大部分情況,對象都會首先在 Eden 區(qū)域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden 區(qū)->Survivor 區(qū)后對象的初始年齡變?yōu)?1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置。

修正(issue552):“Hotspot遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了survivor區(qū)的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值”。

動態(tài)年齡計算的代碼如下

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  //survivor_capacity是survivor空間的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
 total += sizes[age];//sizes數(shù)組是每個年齡段對象大小
 if (total > desired_survivor_size) break;
 age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
  ...
}

額外補充說明(issue672):關(guān)于默認的晉升年齡是15,這個說法的來源大部分都是《深入理解Java虛擬機》這本書。 如果你去Oracle的官網(wǎng)閱讀相關(guān)的虛擬機參數(shù),你會發(fā)現(xiàn)-XX:MaxTenuringThreshold=threshold這里有個說明

Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.默認晉升年齡并不都是15,這個是要區(qū)分垃圾收集器的,CMS就是6.

1.5主要進行 gc 的區(qū)域

周志明先生在《深入理解Java虛擬機》第二版中P92如是寫道:

“老年代GC(Major GC/Full GC),指發(fā)生在老年代的GC……”

上面的說法已經(jīng)在《深入理解Java虛擬機》第三版中被改正過來了。感謝R大的回答:


總結(jié):

針對HotSpot VM的實現(xiàn),它里面的GC其實準確分類只有兩大種:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只對新生代進行垃圾收集;
  • 老年代收集(Major GC / Old GC):只對老年代進行垃圾收集。需要注意的是 Major GC 在有的語境中也用于指代整堆收集;
  • 混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集。

整堆收集 (Full GC):收集整個 Java 堆和方法區(qū)。

2 對象已經(jīng)死亡?

堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經(jīng)死亡(即不能再被任何途徑使用的對象)。


2.1 引用計數(shù)法

給對象中添加一個引用計數(shù)器,每當有一個地方引用它,計數(shù)器就加 1;當引用失效,計數(shù)器就減 1;任何時候計數(shù)器為 0 的對象就是不可能再被使用的。

這個方法實現(xiàn)簡單,效率高,但是目前主流的虛擬機中并沒有選擇這個算法來管理內(nèi)存,其最主要的原因是它很難解決對象之間相互循環(huán)引用的問題。 所謂對象之間的相互引用問題,如下面代碼所示:除了對象 objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導致它們的引用計數(shù)器都不為 0,于是引用計數(shù)算法無法通知 GC 回收器回收他們。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

    }
}

2.2 可達性分析算法

這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節(jié)點開始向下搜索,節(jié)點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。

可作為GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧(Native方法)中引用的對象
  • 方法區(qū)中類靜態(tài)屬性引用的對象
  • 方法區(qū)中常量引用的對象

2.3 再談引用

無論是通過引用計數(shù)法判斷對象引用數(shù)量,還是通過可達性分析法判斷對象的引用鏈是否可達,判定對象的存活都與“引用”有關(guān)。

JDK1.2 之前,Java 中引用的定義很傳統(tǒng):如果 reference 類型的數(shù)據(jù)存儲的數(shù)值代表的是另一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表一個引用。

JDK1.2 以后,Java 對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

1.強引用(StrongReference)

以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內(nèi)存空間不足,Java 虛擬機寧愿拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內(nèi)存不足問題。

2.軟引用(SoftReference)

如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內(nèi)存空間足夠,垃圾回收器就不會回收它,如果內(nèi)存空間不足了,就會回收這些對象的內(nèi)存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現(xiàn)內(nèi)存敏感的高速緩存。

軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。

3.弱引用(WeakReference)

如果一個對象只具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程, 因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關(guān)聯(lián)的引用隊列中。

4.虛引用(PhantomReference)

"虛引用"顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

虛引用主要用來跟蹤對象被垃圾回收的活動。

虛引用與軟引用和弱引用的一個區(qū)別在于: 虛引用必須和引用隊列(ReferenceQueue)聯(lián)合使用。當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是否已經(jīng)加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動。

特別注意,在程序設(shè)計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速 JVM 對垃圾內(nèi)存的回收速度,可以維護系統(tǒng)的運行安全,防止內(nèi)存溢出(OutOfMemory)等問題的產(chǎn)生。

2.4 不可達的對象并非“非死不可”

即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標記過程;可達性分析法中不可達的對象被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經(jīng)被虛擬機調(diào)用過時,虛擬機將這兩種情況視為沒有必要執(zhí)行。

被判定為需要執(zhí)行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關(guān)聯(lián),否則就會被真的回收。

2.5 如何判斷一個常量是廢棄常量?

運行時常量池主要回收的是廢棄的常量。那么,我們?nèi)绾闻袛嘁粋€常量是廢棄常量呢?

JDK1.7 及之后版本的 JVM 已經(jīng)將運行時常量池從方法區(qū)中移了出來,在 Java 堆(Heap)中開辟了一塊區(qū)域存放運行時常量池。

修正(issue747,reference):

  1. JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區(qū), 此時hotspot虛擬機對方法區(qū)的實現(xiàn)為永久代
  2. JDK1.7 字符串常量池被從方法區(qū)拿到了堆中, 這里沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區(qū), 也就是hotspot中的永久代
  3. JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區(qū), 只不過方法區(qū)的實現(xiàn)從永久代變成了元空間(Metaspace)

假如在字符串常量池中存在字符串 "abc",如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量,如果這時發(fā)生內(nèi)存回收的話而且有必要的話,"abc" 就會被系統(tǒng)清理出常量池了。

2.6 如何判斷一個類是無用的類

方法區(qū)主要回收的是無用的類,那么如何判斷一個類是無用的類的呢?

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 “無用的類”

  • 該類所有的實例都已經(jīng)被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經(jīng)被回收。
  • 該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。

3 垃圾收集算法

3.1 標記-清除算法

該算法分為“標記”和“清除”階段:首先標記出所有不需要回收的對象,在標記完成后統(tǒng)一回收掉所有沒有被標記的對象。它是最基礎(chǔ)的收集算法,后續(xù)的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:

  1. 效率問題
  2. 空間問題(標記清除后會產(chǎn)生大量不連續(xù)的碎片)

3.2 復制算法

為了解決效率問題,“復制”收集算法出現(xiàn)了。它可以將內(nèi)存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內(nèi)存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內(nèi)存回收都是對內(nèi)存區(qū)間的一半進行回收。


3.3 標記-整理算法

根據(jù)老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內(nèi)存。


3.4 分代收集算法

當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法。

比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。

延伸面試問題: HotSpot 為什么要分為新生代和老年代?

根據(jù)上面的對分代收集算法的介紹回答。

4 垃圾收集器

如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。

雖然我們對各個收集器進行比較,但并非要挑選出一個最好的收集器。因為直到現(xiàn)在為止還沒有最好的垃圾收集器出現(xiàn),更加沒有萬能的垃圾收集器,我們能做的就是根據(jù)具體應(yīng)用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內(nèi)、任何場景下都適用的完美收集器存在,那么我們的 HotSpot 虛擬機就不會實現(xiàn)那么多不同的垃圾收集器了。

4.1 Serial 收集器

Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結(jié)束。

新生代采用復制算法,老年代采用標記-整理算法。

虛擬機的設(shè)計者們當然知道 Stop The World 帶來的不良用戶體驗,所以在后續(xù)的垃圾收集器設(shè)計中停頓時間在不斷縮短(仍然還有停頓,尋找最優(yōu)秀的垃圾收集器的過程仍然在繼續(xù))。

但是 Serial 收集器有沒有優(yōu)于其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對于運行在 Client 模式下的虛擬機來說是個不錯的選擇。

4.2 ParNew 收集器

ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數(shù)、收集算法、回收策略等等)和 Serial 收集器完全一樣。

新生代采用復制算法,老年代采用標記-整理算法。

它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的并發(fā)收集器,后面會介紹到)配合工作。

并行和并發(fā)概念補充:

  • 并行(Parallel) :指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態(tài)。

  • 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行,可能會交替執(zhí)行),用戶程序在繼續(xù)運行,而垃圾收集器運行在另一個 CPU 上。

4.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器也是使用復制算法的多線程收集器,它看上去幾乎和ParNew都一樣。 那么它有什么特別之處呢?

-XX:+UseParallelGC 

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代并行

Parallel Scavenge 收集器關(guān)注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關(guān)注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是 CPU 中用于運行用戶代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了很多參數(shù)供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解,手工優(yōu)化存在困難的時候,使用Parallel Scavenge收集器配合自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理優(yōu)化交給虛擬機去完成也是一個不錯的選擇。

新生代采用復制算法,老年代采用標記-整理算法。

是JDK1.8默認收集器
使用java -XX:+PrintCommandLineFlags -version命令查看

-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

JDK1.8默認使用的是Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC參數(shù),則默認指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC來禁用該功能

4.4.Serial Old 收集器

Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備方案。

4.5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優(yōu)先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。

4.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應(yīng)用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發(fā)收集器,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作。

從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現(xiàn)的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:

  • 初始標記: 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ;
  • 并發(fā)標記: 同時開啟 GC 和用戶線程,用一個閉包結(jié)構(gòu)去記錄可達對象。但在這個階段結(jié)束,這個閉包結(jié)構(gòu)并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發(fā)生引用更新的地方。
  • 重新標記: 重新標記階段就是為了修正并發(fā)標記期間因為用戶程序繼續(xù)運行而導致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發(fā)標記階段時間短
  • 并發(fā)清除: 開啟用戶線程,同時 GC 線程開始對未標記的區(qū)域做清掃。

從它的名字就可以看出它是一款優(yōu)秀的垃圾收集器,主要優(yōu)點:并發(fā)收集、低停頓。但是它有下面三個明顯的缺點:

  • 對 CPU 資源敏感;
  • 無法處理浮動垃圾;
  • 它使用的回收算法-“標記-清除”算法會導致收集結(jié)束時會有大量空間碎片產(chǎn)生。

4.7 G1 收集器

G1 (Garbage-First) 是一款面向服務(wù)器的垃圾收集器,主要針對配備多顆處理器及大容量內(nèi)存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.

被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征。它具備一下特點:

  • 并行與并發(fā):G1 能充分利用 CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動作,G1 收集器仍然可以通過并發(fā)的方式讓 java 程序繼續(xù)執(zhí)行。
  • 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
  • 空間整合:與 CMS 的“標記--清理”算法不同,G1 從整體來看是基于“標記整理”算法實現(xiàn)的收集器;從局部上來看是基于“復制”算法實現(xiàn)的。
  • 可預測的停頓:這是 G1 相對于 CMS 的另一個大優(yōu)勢,降低停頓時間是 G1 和 CMS 共同的關(guān)注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內(nèi)。

G1 收集器的運作大致分為以下幾個步驟:

  • 初始標記
  • 并發(fā)標記
  • 最終標記
  • 篩選回收

G1 收集器在后臺維護了一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)。這種使用 Region 劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了 G1 收集器在有限時間內(nèi)可以盡可能高的收集效率(把內(nèi)存化整為零)。

參考

作者:Snailclimb
鏈接:JVM 垃圾回收
來源:github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容