Java 虛擬機(jī)系列三:垃圾收集器一網(wǎng)打盡,船新的 ZGC 和 Shenandoah?? 聽說過嗎

前言

上篇文章已經(jīng)為大家詳細(xì)介紹了 JVM 的垃圾收集機(jī)制,那么這次就一起來看看這些機(jī)制究竟是怎樣應(yīng)用到具體的垃圾收集器上的吧。Java 語言和 JVM 在不斷迭代發(fā)展的同時(shí),垃圾收集器也在不斷地進(jìn)化,從最初的的單線程收集器 Serial,到后來的并行收集器 Parallel 和并發(fā)收集器 CMS、G1,再到垃圾收集器最前沿成果——超低延遲的 Shenandoah 和 ZGC,還有不做垃圾收集的垃圾收集器 Epsilon (是的你沒有看錯(cuò)),正是有了這些垃圾收集器的存在,Java 開發(fā)者才得以從繁瑣的手動(dòng)管理中解放出來。下面將為大家一一介紹這些垃圾收集器,全文采用“總-分”結(jié)構(gòu),先總體認(rèn)識(shí)一下所有的垃圾收集器,在逐個(gè)進(jìn)行介紹。

一、垃圾收集器匯總

下圖就是 HotSpot 虛擬機(jī)上的已商用的垃圾收集器的關(guān)系圖 (此圖并不包含 Shenandoah 和 ZGC,因?yàn)檫@兩者目前都還處于實(shí)驗(yàn)階段,且沒有遵循經(jīng)典的分代收集理論,另外的 Epsilon 也不是常規(guī)的垃圾收集器,因此也沒出現(xiàn)在此圖上)。

垃圾收集器家族

圖中的連線表示兩個(gè)垃圾收集器之間可以搭配使用,請注意,JDK 9 已不再支持 Serial + CMS 和 ParNew + Serial Old 的搭配組合。如果覺得數(shù)量太多不好記的話,可以把上圖中的五個(gè)垃圾收集器分為以下三大類:

  1. Serial 類:新生代版本為 Serial,老年代版本為 Serial Old,這兩個(gè)都是單線程垃圾收集器。另外,ParNew 相比 Serial 只是增加了多線程并行收集的功能,并無其他太大差別。

  2. Parallel 類:包括 Parallel Scavenge 和 Parallel Old,多線程并行垃圾收集器經(jīng)典組合,這個(gè)組合更注重于提高程序的吞吐量。

  3. 并發(fā)收集器:CMS 和 G1都可以并發(fā)進(jìn)行垃圾收集,其中 CMS 只適用于老年代,而 G1 則橫跨新生代和老年代。

并發(fā) (concurrent)與并行 (parallel):這里所說的并發(fā)與并行的概念和操作系統(tǒng)里的概念有所不同,這里的并發(fā)是指垃圾收集線程和用戶線程可以同時(shí)執(zhí)行,而并行是指多個(gè)垃圾收集線程同時(shí)執(zhí)行,但用戶線程必須暫停。

除了上圖這些經(jīng)典的垃圾收集器,還有一些目前尚處于試驗(yàn)階段的黑科技收集器,這部分僅做了解即可,萬一面試的時(shí)候扯到了,還能順帶裝一波逼。OracleJDK 11 新加入了 ZGC 收集器(目前還處于實(shí)驗(yàn)階段),OpenJDK 12 中也加入了 其獨(dú)有的 Shenandoah 收集器 (也處于實(shí)驗(yàn)階段),OracleJDK 和 OpenJDK 的區(qū)別這里就不細(xì)說了。這兩款垃圾收集器都以超低延遲為賣點(diǎn),也就是盡量縮短垃圾收集時(shí)用戶線程的暫停 (Stop The World)的時(shí)間,這兩款收集器都宣稱可以把垃圾收集的停頓時(shí)間控制在 10 毫秒以內(nèi),比之前最牛X的G1的延遲還要短。最后還有適用于微服務(wù)領(lǐng)域的 Epsilon,下面就為大家一一介紹這些琳瑯滿目、五花八門的垃圾收集器。

二、垃圾收集器詳解

2.1 新生代收集器

2.1.1 Serial 收集器

Serial 收集器是最基礎(chǔ)、歷史最悠久的垃圾收集器,在 JDK 1.3.1 之前是 HotSpot 虛擬機(jī)新生代收集器的唯一選擇。既然如此,也不能指望它有多么強(qiáng)大的功能了,這是一款單線程收集器,不僅只有一個(gè)垃圾收集線程,更難受的是它在進(jìn)行垃圾收集時(shí)必須暫停所有用戶線程,也就是說垃圾收集時(shí)需要全程 “Stop The World”,如圖:

Serial:Serial Old搭配的垃圾收集示意圖

可見 Serial 在進(jìn)行垃圾收集是必須“Stop The World”,而且其單線程的收集效率并不高,可能造成用戶程序的長時(shí)間停頓。上篇文章已經(jīng)給大家介紹過了新生代和老年代的概念,接下來補(bǔ)充一下圖中安全點(diǎn)的概念:

安全點(diǎn) (safepoint):安全點(diǎn)是代碼指令中特定的位置,這些位置記錄著棧和寄存器里那些位置是引用,這樣收集器在掃描垃圾對象時(shí)就不需要一個(gè)不漏地從方法區(qū)等 GC Roots 開始查找。安全點(diǎn)位置一般選在方法調(diào)用、循環(huán)跳轉(zhuǎn)和異常跳轉(zhuǎn)的代碼指令處,因?yàn)檫@些位置的代碼可以“長時(shí)間運(yùn)行”。

2.1.2 ParNew 收集器

ParNew 收集器實(shí)質(zhì)上就是 Serial 收集器的多線程版本,這也是它的唯一優(yōu)勢,除了同時(shí)使用多線程進(jìn)行垃圾收集之外,其他的行為包括 Serial 所有可用的控制參數(shù) (比如 -XX:SurvivorRatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure等),還垃圾收集算法、Stop The World、對象分配規(guī)則、回收策略等都與 Serial 收集器完全一致。這兩個(gè)收集器的底層代碼大部分也是相通的。

ParNew:Serial Old搭配的垃圾收集示意圖

可以使用 -XX:+/-UseParNewGC選項(xiàng)來強(qiáng)制指定或禁用 ParNew 收集器,ParNew 還有一個(gè)特點(diǎn),就是在使用 -XX:UseConcMarkSweepGC 參數(shù)激活 CMS 收集器后,新生代會(huì)默認(rèn)使用 ParNew 收集器。

2.1.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一款作用于新生代、基于標(biāo)記-復(fù)制算法的多線程并行垃圾收集器,與 ParNew 有很多相似之處。相比 CMS、G1、Shenandoah 和ZGC 這些致力于降低停頓時(shí)間,也就是低延遲的收集器,Parallel Scavenge 是吞吐量 (throughput)優(yōu)先的收集器,吞吐量是指 CPU 用于運(yùn)行用戶程序的時(shí)間與 CPU 總消耗時(shí)間的比值:

吞吐量

低延遲和高吞吐量的收集器有著不同的適用場景,前者適用于與用戶交互較多或需要保證服務(wù)器響應(yīng)質(zhì)量的場景,低延遲可以帶來良好的用戶體驗(yàn),而高吞吐量可以讓 CPU 把更多的時(shí)間用在運(yùn)行用戶程序上面,可以更快完成任務(wù),適用于交互性不強(qiáng)的后臺(tái)運(yùn)算場景。Parallel Scavenge 提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的 -XX:MaxGCPauseMillis參數(shù)和直接設(shè)置吞吐量大小的 -XX:GCTimeRatio。

Parallel Scavenge 還有一個(gè)比較特色的開關(guān)參數(shù):-XX:+UseAdaptiveSizePolicy,激活這個(gè)參數(shù)后,會(huì)開啟自適應(yīng)策略,也就是無需我們手動(dòng)設(shè)置新生代大小 (-Xmn)、Eden 與 Survivor 的比例 (-XX:SurvivorRatio)和直接晉升老年代對象大?。?XX:PretenureSizeThreshold),虛擬機(jī)會(huì)根據(jù)系統(tǒng)運(yùn)行狀態(tài)并收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量。

2.2 老年代收集器

2.2.1 Serial Old 收集器

這就是 Serial 的老年代版本,也是單線程收集器,使用標(biāo)記-整理算法,是 Serial 的黃金搭檔:

Serial:Serial Old搭配的垃圾收集示意圖

這個(gè)搭檔主要用在客戶端模式下,除此之外,Serial Old 還有兩個(gè)用途,那就是和 JDK 5 及之前的 Parallel Scavenge搭配使用,以及作為 CMS 收集器失敗之后的備胎。

2.2.2 Parallel Old 收集器

這個(gè)是 Parallel Scavenge 的老年代版本,但是直到 JDK 6 才正式提供,之前的 Parallel Scavenge 只能和單線程的 Serial Old 搭配使用,完全發(fā)揮不了其優(yōu)勢,Parallel Old 出現(xiàn)后,“吞吐量優(yōu)先”收集器終于也有了黃金搭檔:

Parallel Scavenge 和 Parallel Old 搭配的垃圾收集過程

2.2.3 CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一款致力于獲取最短停頓時(shí)間的收集器,從它的名字中可以看出這款收集器有兩個(gè)重要特點(diǎn):一,這是一款可以并發(fā)進(jìn)行垃圾收集的收集器;二,這款收集器是基于標(biāo)記清除-算法的。它的運(yùn)作過程相對于之前不能并發(fā)的垃圾收集器更加復(fù)雜,大體分為以下四個(gè)步驟:

  1. 初始標(biāo)記 (CMS initial mark)

    這一步僅僅是標(biāo)記一下與 GC Roots 直接關(guān)聯(lián)的對象,雖然不是并發(fā)執(zhí)行,但是速度很快,用戶程序會(huì)有短暫的暫停。

  2. 并發(fā)標(biāo)記 (CMS concurrent mark)

    這一步比較耗時(shí),需要遍歷所有與 GC Roots 有關(guān)聯(lián)的對象,但是可以與用戶線程并發(fā)執(zhí)行,所以對用戶程序影響不大。

  3. 重新標(biāo)記 (CMS remark)

    由于并發(fā)標(biāo)記過程中用戶程序是不暫停的,所以有可能引起原來的標(biāo)記對象產(chǎn)生變動(dòng),而重新標(biāo)記的作用就是修正那些變動(dòng)的標(biāo)記記錄,這一階段雖然無法并發(fā)執(zhí)行,但是工作量很小,所以持續(xù)時(shí)間也很短。

  4. 并發(fā)清除 (CMS concurrent sweep)

    這一階段就是清除掉可回收的對象,回想上篇文章介紹的標(biāo)記-清除算法,在清除掉垃圾對象后并不需要移動(dòng)存活對象,所以這一階段可以與用戶線程并發(fā)執(zhí)行。

CMS 垃圾收集過程

綜上,CMS 收集器在運(yùn)行過程中只需在初始標(biāo)記階段和重新標(biāo)記階段暫停用戶程序,而且時(shí)間很短,其他階段均可與用戶程序并發(fā)執(zhí)行,這就是它實(shí)現(xiàn)超短停頓的秘密所在。

CMS 的優(yōu)勢很明顯,就是并發(fā)收集和低停頓,但也不是完美無缺的,它主要有以下三個(gè)明顯缺點(diǎn):

  1. CMS 收集器對處理器資源,也就是 CPU 核心數(shù)非常敏感,這也是所有并發(fā)設(shè)計(jì)的程序的共同特點(diǎn)。雖然它并發(fā)特點(diǎn)帶來了低停頓的優(yōu)勢,但是由于擠占了處理器資源,導(dǎo)致總吞吐量降低,程序運(yùn)行總時(shí)間也會(huì)相應(yīng)延長。

  2. CMS 無法處理“浮動(dòng)垃圾”,因?yàn)椴l(fā)標(biāo)記階段和并發(fā)清除階段用戶程序是在繼續(xù)執(zhí)行的,自然會(huì)繼續(xù)產(chǎn)生垃圾對象,但是這些垃圾對象產(chǎn)生在標(biāo)記階段之后,所以無法被標(biāo)記出來,自然也就無法被清除,而這可能會(huì)引發(fā)停頓時(shí)間較長的 Full GC。

  3. 空間碎片,既然 CMS 是基于標(biāo)記-清除算法的,也就不能避免產(chǎn)生空間碎片了,空間碎片就是不連續(xù)的可用內(nèi)存,這可能導(dǎo)致明明有剩余空間,但就是放不下新對象,從而提前觸發(fā) Full GC。

2.3 G1 收集器

Garbage First 收集器,簡稱 G1,可以說是垃圾收集器技術(shù)史上里程碑式的成果,它開創(chuàng)了收集器面向局部收集的設(shè)計(jì)思路和基于 Region 的內(nèi)存布局結(jié)構(gòu)。也是從 G1 開始,垃圾收集器,包括后來的 Shenandoah 和 ZGC 都不再局限于只回收新生代或只回收老年代,而是面向整個(gè) Java 堆。

G1 收集器最大的特色就是可預(yù)測的停頓,用戶可以通過 -XX:MaxPauseMillis 參數(shù) (默認(rèn)200毫秒)指定期望的最大停頓時(shí)間,但不能隨意指定,要切合實(shí)際,然后 G1 會(huì)根據(jù)這一目標(biāo)值篩選并回收那些回收價(jià)值最高的可回收對象,那么 G1 是怎樣做到這一點(diǎn)的呢?關(guān)鍵就在于 G1 基于 Region 的內(nèi)存布局,先來看一下 G1 和之前垃圾收集器的堆內(nèi)存布局對比:

G1 之前各款垃圾收集器的堆內(nèi)存布局
G1 收集器堆內(nèi)存布局

由此可見,雖然 G1 仍然遵循分帶收集理論,但是內(nèi)存區(qū)域不再按照固定大小的新生代和老年代進(jìn)行劃分,而是把連續(xù)的 Java 對劃分成多個(gè)大小相等的獨(dú)立區(qū)域 (Region),每一個(gè) Region 都可以根據(jù)需要扮演新生代的 Eden 空間、Survivor 空間或老年代空間。收集器可以對扮演不同角色的 Region 采用不同的策略進(jìn)行處理,這樣無論是新創(chuàng)建的對象還是已經(jīng)存活了一段時(shí)間的對象,抑或是熬過了多次收集的就對象都能獲得很好的收集效果。Region 中還有一類特殊的 Humongous 區(qū)域,專門用來存放大對象,G1 認(rèn)為只要大小超過一個(gè) Region 容量一半的對象就可判定為大對象,每個(gè) Region 的大小可以通過參數(shù) -XX:G1HeapRedionSize 設(shè)定,取值范圍為 1MB~32MB,且為 2 的 N 次冪,對于那些大小超過整個(gè) Region 大小的超大對象,將會(huì)被存放在 N 個(gè)連續(xù)的 Humongous Region 中,G1 一般會(huì)把 Humongous Region 看做老年代。

在把內(nèi)存分成 Region 管理之后,G1 就可以對這些 Region 各個(gè)擊破了,其停頓時(shí)間之所以可控,是因?yàn)?G1 在垃圾收集時(shí)并不會(huì)把整個(gè) Java 堆當(dāng)做回收區(qū)域,而是只收集那些回收價(jià)值最高的 Region,保證能在指定最大停頓時(shí)間內(nèi)回收完畢,回收價(jià)值是指回收所獲得的空間大小及耗費(fèi)時(shí)間的權(quán)衡結(jié)果。這樣就保證了 G1 能在指定時(shí)間內(nèi)獲得盡可能高的回收效率。

G1 的回收過程大致可以分為以下四個(gè)步驟:

  1. 初始標(biāo)記 (initial marking):僅僅標(biāo)記直接與 GC Roots 關(guān)聯(lián)的對象,停頓時(shí)間很短;

  2. 并發(fā)標(biāo)記 (Concurrent Marking):從 GC Roots 開始對堆中對象進(jìn)行可達(dá)性分析,耗時(shí)較長,但能與用戶程序并發(fā)執(zhí)行,所以不會(huì)停頓;

  3. 最終標(biāo)記 (Final Marking):用戶程序并發(fā)執(zhí)行導(dǎo)致對象引用關(guān)系變化,修正變化的引用,此階段會(huì)短暫地暫停用戶程序;

  4. 篩選回收 (Live Data Counting and Evacuation):更新 Region 的統(tǒng)計(jì)數(shù)據(jù),對各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序,并根據(jù)用戶所期望的停頓時(shí)間指定回收計(jì)劃,可以自由選擇任意多個(gè) Region 構(gòu)成回收集,然后把需要回收的 Region 中存貨對象復(fù)制到空的 Region 中,在清理掉舊 Region 的全部空間,這里涉及存活對象的移動(dòng),必須暫停用戶線程,是由多條收集器線程并行完成的。

G1 收集器運(yùn)行過程

G1 和 CMS 都是以低停頓為目標(biāo)的收集器,所以經(jīng)常被拿來比較孰優(yōu)孰劣,雖然 G1 相比 CMS 優(yōu)勢明顯,但也并非全方位的碾壓,G1相比 CMS 的優(yōu)缺點(diǎn)如下:

  • G1 優(yōu)點(diǎn):

    1. 可以指定最大停頓時(shí)間;

    2. 分 Region 管理內(nèi)存,按受益動(dòng)態(tài)確定回收區(qū)域;

    3. 不會(huì)產(chǎn)生內(nèi)存碎片:G1 的內(nèi)存布局并不是固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域 (Region),G1 從整體來看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,但從局部 (兩個(gè)Region 之間)上看又是基于“標(biāo)記-復(fù)制”算法實(shí)現(xiàn),不會(huì)像 CMS (“標(biāo)記-清除”算法) 那樣產(chǎn)生內(nèi)存碎片。

  • G1 缺點(diǎn):

    G1 需要記憶集 (具體來說是卡表)來記錄新生代和老年代之間的引用關(guān)系,這種數(shù)據(jù)結(jié)構(gòu)在 G1 中需要占用大量的內(nèi)存,可能達(dá)到整個(gè)堆內(nèi)存容量的 20% 甚至更多。而且 G1 中維護(hù)記憶集的成本較高,帶來了更高的執(zhí)行負(fù)載,影響效率。

按照《深入理解Java虛擬機(jī)》作者的說法,CMS 在小內(nèi)存應(yīng)用上的表現(xiàn)要優(yōu)于 G1,而大內(nèi)存應(yīng)用上 G1 更有優(yōu)勢,大小內(nèi)存的分水嶺是6GB到8GB。

2.4 最前沿科技成果:低延遲垃圾收集器

之前最先進(jìn)的 G1 收集器早在 JDK 7 上就已經(jīng)發(fā)布了成熟版,而截至目前的2020年初,JDK 版本已經(jīng)來到了 JDK 13,與此同時(shí),垃圾收集器領(lǐng)域也早已有了更先進(jìn)的黑科技,其中的代表者就是號(hào)稱可以將停頓時(shí)間控制在10毫秒內(nèi)低延遲收集器——Shenandoah 和 ZGC,它們最牛X的地方在于并發(fā)程度更高,連移動(dòng)存活對象 (也就是標(biāo)記-整理算法的整理階段)都可以做到并發(fā)執(zhí)行 (不過二者的實(shí)現(xiàn)原理有所區(qū)別):

各種垃圾收集器并發(fā)程度對比,綠色表示并發(fā),黃色表示非并發(fā)

由圖可知,相比之前的收集器,Shenandoah 和 ZGC 在工作過程中幾乎全程并發(fā),只有在初始標(biāo)記、最終標(biāo)記這些階段有短暫的暫停,而且這些停頓時(shí)間與堆容量和堆中對象數(shù)量沒有正比例關(guān)系,這才可以將停頓時(shí)間控制在驚人的10毫秒以內(nèi)。

2.4.1 Shenandoah 收集器

Shenandoah 是由 ReadHat 公司獨(dú)立發(fā)展的新型垃圾收集器,并在2014年貢獻(xiàn)給了 OpenJDK,并成為 OpenJDK 12 的正式特性之一,但是以 Oracle 公司的尿性,卻不愿把它添加到 OracleJDK 中,這也導(dǎo)致了免費(fèi)開源的 OpenJDK 反而比商業(yè)收費(fèi)的 OracleJDK 功能更多,實(shí)屬罕見。

Shenandoah 與 G1 有很多相似之處,比如都是基于 Region 的內(nèi)存布局,都有用于存放大對象的 Humongous Region,默認(rèn)回收策略也是優(yōu)先處理回收價(jià)值最大的 Region。不過也有三個(gè)重大的區(qū)別:

  1. 最最重要的區(qū)別,Shenandoah 支持并發(fā)的整理算法,G1 的整理階段雖是多線程并行,但無法與用戶程序并發(fā)執(zhí)行;

  2. 默認(rèn)不使用分代收集理論;

  3. 使用連接矩陣 (Connection Matrix)記錄跨 Region 的引用關(guān)系,替換掉了 G1 中的記憶級(jí) (Remembered Set),內(nèi)存和計(jì)算成本更低。

Shenandoah 收集器的工作原理相比 G1 要復(fù)雜不少,其運(yùn)行流程示意圖如下:

Shenandoah 收集器運(yùn)行流程.png

可見 Shenandoah 的并發(fā)程度明顯比 G1 更高,只需要在初始標(biāo)記、最終標(biāo)記、初始引用更新和最終引用更新這幾個(gè)階段進(jìn)行短暫的“Stop The World”,其他階段皆可與用戶程序并發(fā)執(zhí)行,其中最重要的并發(fā)標(biāo)記、并發(fā)回收和并發(fā)引用更新詳情如下:

  • 并發(fā)標(biāo)記( Concurrent Marking)

    與G1一樣,遍歷對象圖,標(biāo)記出全部可達(dá)的對象,這個(gè)階段是與用戶線程一起并發(fā)的,時(shí)間長短取決于堆中存活對象的數(shù)量以及對象圖的結(jié)構(gòu)復(fù)雜程度。

  • 并發(fā)回收( Concurrent Evacuation)

    并發(fā)回收階段是 Shenandoah 與之前 HotSpot 中其他收集器的核心差異。在這個(gè)階段, Shenandoah 要把待回收 Region 里面的存活對象先復(fù)制一份到其他未被使用的 Region之中。復(fù)制對象這件事情如果將用戶線程凍結(jié)起來再做那是相當(dāng)簡單的,但如果兩者必須要同時(shí)并發(fā)進(jìn)行的話,就變得復(fù)雜起來了。其困難點(diǎn)是在移動(dòng)對象的同時(shí),用戶線程仍然可能不停對被移動(dòng)的對象進(jìn)行讀寫訪問,移動(dòng)對象是一次性的行為,但移動(dòng)之后整個(gè)內(nèi)存中所有指向該對象的引用都還是舊對象的地址,這是很難一瞬間全部改變過來的。對于并發(fā)回收階段遇到的這些困難, Shenandoah 將會(huì)通過讀屏障和被稱為“ Brooks Pointers”的轉(zhuǎn)發(fā)指針來解決。并發(fā)回收階段運(yùn)行的時(shí)間長短取決于回收集的大小。

    Brooks Pointers 簡要介紹:這是一種轉(zhuǎn)發(fā)指針 (Forwarding Pointer),原理就是在所有的對象上新添加一個(gè)指針,初始狀態(tài)下該指針指向?qū)ο蟊旧?,而在垃圾回收過程中,如果該對象是存活對象,則需要將其從回收區(qū)域移動(dòng)到目標(biāo)區(qū)域 (其實(shí)就是在目標(biāo)區(qū)域復(fù)制一個(gè)新對象,這就是標(biāo)記-整理算法的整理階段,之前的 G1 收集器在此階段無法與用戶程序并發(fā)執(zhí)行),然后把舊對象的轉(zhuǎn)發(fā)指針指向新的對象,這樣用戶程序在并發(fā)執(zhí)行的情況下,就不會(huì)訪問到舊對象了。

  • 并發(fā)引用更新( Concurrent Update Reference)

    這個(gè)階段是與用戶線程一起并發(fā)的,時(shí)間長短取決于內(nèi)存中涉及的引用數(shù)量的多少。并發(fā)引用更新與并發(fā)標(biāo)記不同,它不再需要沿著對象圖來搜索,只需要按照內(nèi)存物理地址的順序,線性地搜索出引用類型,把舊值改為新值即可。

Shenandoah 的高并發(fā)度讓它實(shí)現(xiàn)了超低的停頓時(shí)間,但是更高的復(fù)雜度也伴隨著更高的系統(tǒng)開銷,這在一定程度上會(huì)影響吞吐量,下圖是 Shenandoah 與之前各種收集器在停頓時(shí)間維度和系統(tǒng)開銷維度上的對比:

Shenandoah 與之前各款收集器的對比

OracleJDK 并不支持 Shenandoah,如果你用的是 OpenJDK 12 或某些支持 Shenandoah 移植版的 JDK 的話,可以通過以下參數(shù)開啟 Shenandoah:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

2.4.2 ZGC 收集器

Z Garbage Collector,簡稱 ZGC,是 JDK 11 中新加入的尚在實(shí)驗(yàn)階段的低延遲垃圾收集器。它和 Shenandoah 同屬于超低延遲的垃圾收集器,但在吞吐量上比 Shenandoah 有更優(yōu)秀的表現(xiàn),甚至超過了 G1,接近了“吞吐量優(yōu)先”的 Parallel 收集器組合,可以說近乎實(shí)現(xiàn)了“魚與熊掌兼得”。

ZGC 的內(nèi)存布局

與 Shenandoah 和 G1 一樣,ZGC 也采用基于 Region 的堆內(nèi)存布局,但與它們不同的是, ZGC 的 Region 具有動(dòng)態(tài)性,也就是可以動(dòng)態(tài)創(chuàng)建和銷毀,容量大小也是動(dòng)態(tài)的,有大、中、小三類容量:

ZGC 內(nèi)存布局
  • 小型 Region (Small Region):容量固定為 2MB,用于放置小于 256KB 的小對象。

  • 中型 Region (M edium Region):容量固定為 32MB,用于放置大于等于 256KB 但小于 4MB 的對 象。

  • 大型 Region (Large Region):容量不固定,可以動(dòng)態(tài)變化,但必須為 2MB 的整數(shù)倍,用于放置 4MB 或以上的大對象。每個(gè)大型 Region 中只會(huì)存放一個(gè)大對象,這也預(yù)示著雖然名字叫作“大型 Region”,但它的實(shí)際容量完全有可能小于中型 Region,最小容量可低至 4MB。

與 Shenandoah 一樣,ZGC 在工作過程中也幾乎是全程與用戶程序并發(fā)的,重點(diǎn)也是實(shí)現(xiàn)了標(biāo)記-整理算法的整理階段可以與用戶程序并發(fā)執(zhí)行。但是二者的實(shí)現(xiàn)方式不同,Shenandoah 是在對象身上添加轉(zhuǎn)發(fā)指針的方法,而 ZGC 則是直接在指針上動(dòng)手腳,也就是傳說中的染色指針 (Colored Pointers),這個(gè)指針就是 Java 對象的引用,例如:

Object o = new Object();

其中“o” 只是一個(gè)引用,也就是指針,指向存在堆上的對象實(shí)例,引用自身也是要占內(nèi)存的,普通引用在32位機(jī)器占4個(gè)字節(jié),在64位機(jī)器上,開啟壓縮指針 (-XX:+UseCompressedOops) 的話占4個(gè)字節(jié),不開啟的話占8個(gè)字節(jié)。ZGC 的染色指針結(jié)構(gòu)如下 (不支持32位機(jī)器和壓縮指針):

染色指針結(jié)構(gòu)示意圖

得益于染色指針上標(biāo)志位的支持,ZGC 也可以像 Shenandoah 那樣,實(shí)現(xiàn)了在移動(dòng)存活對象的過程中可以與用戶程序并發(fā)執(zhí)行,且效率更高。ZGC 還用到了很多其他的黑科技,原理過于復(fù)雜,就不在這里詳述了。

在 JDK 11 及以上版本,可以通過以下參數(shù)開啟 ZGC:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

2.5 最奇葩的垃圾收集器——Epsilon

上面介紹的各種收集器,比如 G1、Shenandoah 和 ZGC 等都是越來越復(fù)雜,越來越先進(jìn), 而 JDK 11 新加入的 Epsilon 卻是反其道而行,這款收集器不會(huì)做任何垃圾收集的操作,也許叫做“內(nèi)存分配器”更加合適。雖然很奇葩,但是它還是有用武之地的,比如越來越火的微服務(wù)領(lǐng)域,如果系統(tǒng)運(yùn)行時(shí)間很短,在堆內(nèi)存耗盡之前就可以結(jié)束,那么垃圾收集也就沒有任何意義了,這正是 Epsilon 的使用場景。

總結(jié)

本文為大家介紹了目前 HotSpot 虛擬機(jī)上的所有垃圾收集器,有的已經(jīng)久經(jīng)沙場,有的仍處于試驗(yàn)階段,但有望在未來成為主流,在實(shí)際應(yīng)用中,大家可以根據(jù)具體場景選擇合適的垃圾收集器。

參考資料:

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

相關(guān)閱讀更多精彩內(nèi)容

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