Java 虛擬機系列一:一文搞懂 JVM 架構和運行時數據區 (內存區域)
Java 虛擬機系列二:垃圾收集機制詳解,動圖幫你理解
Java 虛擬機系列三:垃圾收集器一網打盡,船新的 ZGC 和 Shenandoah 據說過嗎java
上篇文章已經爲你們詳細介紹了 JVM 的垃圾收集機制,那麼此次就一塊兒來看看這些機制到底是怎樣應用到具體的垃圾收集器上的吧。Java 語言和 JVM 在不斷迭代發展的同時,垃圾收集器也在不斷地進化,從最初的的單線程收集器 Serial,到後來的並行收集器 Parallel 和併發收集器 CMS、G1,再到垃圾收集器最前沿成果——超低延遲的 Shenandoah 和 ZGC,還有不作垃圾收集的垃圾收集器 Epsilon (是的你沒有看錯),正是有了這些垃圾收集器的存在,Java 開發者才得以從繁瑣的手動管理中解放出來。下面將爲你們一一介紹這些垃圾收集器,全文采用「總-分」結構,先整體認識一下全部的垃圾收集器,在逐個進行介紹。面試
下圖就是 HotSpot 虛擬機上的已商用的垃圾收集器的關係圖 (此圖並不包含 Shenandoah 和 ZGC,由於這二者目前都還處於實驗階段,且沒有遵循經典的分代收集理論,另外的 Epsilon 也不是常規的垃圾收集器,所以也沒出如今此圖上)。算法
HotSpot 虛擬機的垃圾收集器
圖中的連線表示兩個垃圾收集器之間能夠搭配使用,請注意,JDK 9 已再也不支持 Serial + CMS 和 ParNew + Serial Old 的搭配組合。若是以爲數量太多很差記的話,能夠把上圖中的五個垃圾收集器分爲如下三大類:segmentfault
併發 (concurrent)與並行 (parallel):這裏所說的併發與並行的概念和操做系統裏的概念有所不一樣,這裏的併發是指垃圾收集線程和用戶線程能夠同時執行,而並行是指多個垃圾收集線程同時執行,但用戶線程必須暫停。
除了上圖這些經典的垃圾收集器,還有一些目前尚處於試驗階段的黑科技收集器,這部分僅作了解便可,萬一面試的時候扯到了,還能順帶裝一波逼。OracleJDK 11 新加入了 ZGC 收集器(目前還處於實驗階段),OpenJDK 12 中也加入了 其獨有的 Shenandoah 收集器 (也處於實驗階段),OracleJDK 和 OpenJDK 的區別這裏就不細說了。這兩款垃圾收集器都以超低延遲爲賣點,也就是儘可能縮短垃圾收集時用戶線程的暫停 (Stop The World)的時間,這兩款收集器都宣稱能夠把垃圾收集的停頓時間控制在 10 毫秒之內,比以前最牛X的G1的延遲還要短。最後還有適用於微服務領域的 Epsilon,下面就爲你們一一介紹這些琳琅滿目、五花八門的垃圾收集器。安全
Serial 收集器是最基礎、歷史最悠久的垃圾收集器,在 JDK 1.3.1 以前是 HotSpot 虛擬機新生代收集器的惟一選擇。既然如此,也不能期望它有多麼強大的功能了,這是一款單線程收集器,不只只有一個垃圾收集線程,更難受的是它在進行垃圾收集時必須暫停全部用戶線程,也就是說垃圾收集時須要全程 「Stop The World」,如圖:服務器
Serial / Serial Old 搭配的垃圾收集示意圖
可見 Serial 在進行垃圾收集是必須「Stop The World」,並且其單線程的收集效率並不高,可能形成用戶程序的長時間停頓。上篇文章已經給你們介紹過了新生代和老年代的概念,接下來補充一下圖中安全點的概念:數據結構
安全點 (safepoint):安全點是代碼指令中特定的位置,這些位置記錄着棧和寄存器裏那些位置是引用,這樣收集器在掃描垃圾對象時就不須要一個不漏地從方法區等 GC Roots 開始查找。安全點位置通常選在方法調用、循環跳轉和異常跳轉的代碼指令處,由於這些位置的代碼能夠「長時間運行」。
ParNew 收集器實質上就是 Serial 收集器的多線程版本,這也是它的惟一優點,除了同時使用多線程進行垃圾收集以外,其餘的行爲包括 Serial 全部可用的控制參數 (好比 -XX:SurvivorRatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure等),還垃圾收集算法、Stop The World、對象分配規則、回收策略等都與 Serial 收集器徹底一致。這兩個收集器的底層代碼大部分也是相通的。多線程
ParNew / Serial Old 搭配的垃圾收集示意圖
可使用 -XX:+/-UseParNewGC選項來強制指定或禁用 ParNew 收集器,ParNew 還有一個特色,就是在使用 -XX:UseConcMarkSweepGC 參數激活 CMS 收集器後,新生代會默認使用 ParNew 收集器。架構
Parallel Scavenge 收集器也是一款做用於新生代、基於標記-複製算法的多線程並行垃圾收集器,與 ParNew 有不少類似之處。相比 CMS、G一、Shenandoah 和ZGC 這些致力於下降停頓時間,也就是低延遲的收集器,Parallel Scavenge 是吞吐量 (throughput)優先的收集器,吞吐量是指 CPU 用於運行用戶程序的時間與 CPU 總消耗時間的比值:併發
吞吐量計算表達式
低延遲和高吞吐量的收集器有着不一樣的適用場景,前者適用於與用戶交互較多或須要保證服務器響應質量的場景,低延遲能夠帶來良好的用戶體驗,而高吞吐量可讓 CPU 把更多的時間用在運行用戶程序上面,能夠更快完成任務,適用於交互性不強的後臺運算場景。Parallel Scavenge 提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis參數和直接設置吞吐量大小的 -XX:GCTimeRatio。
Parallel Scavenge 還有一個比較特點的開關參數:-XX:+UseAdaptiveSizePolicy,激活這個參數後,會開啓自適應策略,也就是無需咱們手動設置新生代大小 (-Xmn)、Eden 與 Survivor 的比例 (-XX:SurvivorRatio)和直接晉升老年代對象大小(-XX:PretenureSizeThreshold),虛擬機會根據系統運行狀態並收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量。
這就是 Serial 的老年代版本,也是單線程收集器,使用標記-整理算法,是 Serial 的黃金搭檔:
Serial / Serial Old 搭配的垃圾收集示意圖
這個搭檔主要用在客戶端模式下,除此以外,Serial Old 還有兩個用途,那就是和 JDK 5 及以前的 Parallel Scavenge搭配使用,以及做爲 CMS 收集器失敗以後的備胎。
這個是 Parallel Scavenge 的老年代版本,可是直到 JDK 6 才正式提供,以前的 Parallel Scavenge 只能和單線程的 Serial Old 搭配使用,徹底發揮不了其優點,Parallel Old 出現後,「吞吐量優先」收集器終於也有了黃金搭檔:
Parallel Scavenge / Parallel Old 搭配的垃圾收集過程
CMS (Concurrent Mark Sweep) 收集器是一款致力於獲取最短停頓時間的收集器,從它的名字中能夠看出這款收集器有兩個重要特色:一,這是一款能夠併發進行垃圾收集的收集器;二,這款收集器是基於標記清除-算法的。它的運做過程相對於以前不能併發的垃圾收集器更加複雜,大致分爲如下四個步驟:
這一步僅僅是標記一下與 GC Roots 直接關聯的對象,雖然不是併發執行,可是速度很快,用戶程序會有短暫的暫停。
這一步比較耗時,須要遍歷全部與 GC Roots 有關聯的對象,可是能夠與用戶線程併發執行,因此對用戶程序影響不大。
因爲併發標記過程當中用戶程序是不暫停的,因此有可能引發原來的標記對象產生變更,而從新標記的做用就是修正那些變更的標記記錄,這一階段雖然沒法併發執行,可是工做量很小,因此持續時間也很短。
這一階段就是清除掉可回收的對象,回想上篇文章介紹的標記-清除算法,在清除掉垃圾對象後並不須要移動存活對象,因此這一階段能夠與用戶線程併發執行。
CMS 垃圾收集過程
綜上,CMS 收集器在運行過程當中只需在初始標記階段和從新標記階段暫停用戶程序,並且時間很短,其餘階段都可與用戶程序併發執行,這就是它實現超短停頓的祕密所在。
CMS 的優點很明顯,就是併發收集和低停頓,但也不是天衣無縫的,它主要有如下三個明顯缺點:
Garbage First 收集器,簡稱 G1,能夠說是垃圾收集器技術史上里程碑式的成果,它開創了收集器面向局部收集的設計思路和基於 Region 的內存佈局結構。也是從 G1 開始,垃圾收集器,包括後來的 Shenandoah 和 ZGC 都再也不侷限於只回收新生代或只回收老年代,而是面向整個 Java 堆。
G1 收集器最大的特點就是可預測的停頓,用戶能夠經過 -XX:MaxPauseMillis 參數 (默認200毫秒)指按期望的最大停頓時間,但不能隨意指定,要切合實際,而後 G1 會根據這一目標值篩選並回收那些回收價值最高的可回收對象,那麼 G1 是怎樣作到這一點的呢?關鍵就在於 G1 基於 Region 的內存佈局,先來看一下 G1 和以前垃圾收集器的堆內存佈局對比:
G1 以前各款垃圾收集器的堆內存佈局
G1 收集器堆內存佈局
因而可知,雖然 G1 仍然遵循分帶收集理論,可是內存區域再也不按照固定大小的新生代和老年代進行劃分,而是把連續的 Java 對劃分紅多個大小相等的獨立區域 (Region),每個 Region 均可以根據須要扮演新生代的 Eden 空間、Survivor 空間或老年代空間。收集器能夠對扮演不一樣角色的 Region 採用不一樣的策略進行處理,這樣不管是新建立的對象仍是已經存活了一段時間的對象,抑或是熬過了屢次收集的就對象都能得到很好的收集效果。Region 中還有一類特殊的 Humongous 區域,專門用來存放大對象,G1 認爲只要大小超過一個 Region 容量一半的對象就可斷定爲大對象,每一個 Region 的大小能夠經過參數 -XX:G1HeapRedionSize 設定,取值範圍爲 1MB~32MB,且爲 2 的 N 次冪,對於那些大小超過整個 Region 大小的超大對象,將會被存放在 N 個連續的 Humongous Region 中,G1 通常會把 Humongous Region 看作老年代。
在把內存分紅 Region 管理以後,G1 就能夠對這些 Region 各個擊破了,其停頓時間之因此可控,是由於 G1 在垃圾收集時並不會把整個 Java 堆當作回收區域,而是隻收集那些回收價值最高的 Region,保證能在指定最大停頓時間內回收完畢,回收價值是指回收所得到的空間大小及耗費時間的權衡結果。這樣就保證了 G1 能在指定時間內得到儘量高的回收效率。
G1 的回收過程大體能夠分爲如下四個步驟:
G1 收集器運行過程
G1 和 CMS 都是以低停頓爲目標的收集器,因此常常被拿來比較孰優孰劣,雖然 G1 相比 CMS 優點明顯,但也並不是全方位的碾壓,G1相比 CMS 的優缺點以下:
G1 優勢:
G1 須要記憶集 (具體來講是卡表)來記錄新生代和老年代之間的引用關係,這種數據結構在 G1 中須要佔用大量的內存,可能達到整個堆內存容量的 20% 甚至更多。並且 G1 中維護記憶集的成本較高,帶來了更高的執行負載,影響效率。
按照《深刻理解Java虛擬機》做者的說法,CMS 在小內存應用上的表現要優於 G1,而大內存應用上 G1 更有優點,大小內存的分水嶺是6GB到8GB。
以前最早進的 G1 收集器早在 JDK 7 上就已經發布了成熟版,而截至目前的2020年初,JDK 版本已經來到了 JDK 13,與此同時,垃圾收集器領域也早已有了更先進的黑科技,其中的表明者就是號稱能夠將停頓時間控制在10毫秒內低延遲收集器——Shenandoah 和 ZGC,它們最牛X的地方在於併發程度更高,連移動存活對象 (也就是標記-整理算法的整理階段)均可以作到併發執行 (不過兩者的實現原理有所區別):
各類垃圾收集器併發程度對比,綠色表示併發,黃色表示非併發
由圖可知,相比以前的收集器,Shenandoah 和 ZGC 在工做過程當中幾乎全程併發,只有在初始標記、最終標記這些階段有短暫的暫停,並且這些停頓時間與堆容量和堆中對象數量沒有正比例關係,這才能夠將停頓時間控制在驚人的10毫秒之內。
Shenandoah 是由 ReadHat 公司獨立發展的新型垃圾收集器,並在2014年貢獻給了 OpenJDK,併成爲 OpenJDK 12 的正式特性之一,可是以 Oracle 公司的尿性,卻不肯把它添加到 OracleJDK 中,這也致使了免費開源的 OpenJDK 反而比商業收費的 OracleJDK 功能更多,實屬罕見。
Shenandoah 與 G1 有不少類似之處,好比都是基於 Region 的內存佈局,都有用於存放大對象的 Humongous Region,默認回收策略也是優先處理回收價值最大的 Region。不過也有三個重大的區別:
Shenandoah 收集器的工做原理相比 G1 要複雜很多,其運行流程示意圖以下:
Shenandoah 收集器運行流程
可見 Shenandoah 的併發程度明顯比 G1 更高,只須要在初始標記、最終標記、初始引用更新和最終引用更新這幾個階段進行短暫的「Stop The World」,其餘階段皆可與用戶程序併發執行,其中最重要的併發標記、併發回收和併發引用更新詳情以下:
與G1同樣,遍歷對象圖,標記出所有可達的對象,這個階段是與用戶線程一塊兒併發的,時間長短取決於堆中存活對象的數量以及對象圖的結構複雜程度。
併發回收( Concurrent Evacuation)
併發回收階段是 Shenandoah 與以前 HotSpot 中其餘收集器的核心差別。在這個階段, Shenandoah 要把待回收 Region 裏面的存活對象先複製一份到其餘未被使用的 Region之中。複製對象這件事情若是將用戶線程凍結起來再作那是至關簡單的,但若是二者必需要同時併發進行的話,就變得複雜起來了。其困難點是在移動對象的同時,用戶線程仍然可能不停對被移動的對象進行讀寫訪問,移動對象是一次性的行爲,但移動以後整個內存中全部指向該對象的引用都仍是舊對象的地址,這是很難一瞬間所有改變過來的。對於併發回收階段遇到的這些困難, Shenandoah 將會經過讀屏障和被稱爲「 Brooks Pointers」的轉發指針來解決。併發回收階段運行的時間長短取決於回收集的大小。
Brooks Pointers 簡要介紹:這是一種轉發指針 (Forwarding Pointer),原理就是在全部的對象上新添加一個指針,初始狀態下該指針指向對象自己,而在垃圾回收過程當中,若是該對象是存活對象,則須要將其從回收區域移動到目標區域 (其實就是在目標區域複製一個新對象,這就是標記-整理算法的整理階段,以前的 G1 收集器在此階段沒法與用戶程序併發執行),而後把舊對象的轉發指針指向新的對象,這樣用戶程序在併發執行的狀況下,就不會訪問到舊對象了。
這個階段是與用戶線程一塊兒併發的,時間長短取決於內存中涉及的引用數量的多少。併發引用更新與併發標記不一樣,它再也不須要沿着對象圖來搜索,只須要按照內存物理地址的順序,線性地搜索出引用類型,把舊值改成新值便可。
Shenandoah 的高併發度讓它實現了超低的停頓時間,可是更高的複雜度也伴隨着更高的系統開銷,這在必定程度上會影響吞吐量,下圖是 Shenandoah 與以前各類收集器在停頓時間維度和系統開銷維度上的對比:
Shenandoah 與以前各類收集器在停頓時間維度和系統開銷維度上的對比
OracleJDK 並不支持 Shenandoah,若是你用的是 OpenJDK 12 或某些支持 Shenandoah 移植版的 JDK 的話,能夠經過如下參數開啓 Shenandoah:
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
Z Garbage Collector,簡稱 ZGC,是 JDK 11 中新加入的尚在實驗階段的低延遲垃圾收集器。它和 Shenandoah 同屬於超低延遲的垃圾收集器,但在吞吐量上比 Shenandoah 有更優秀的表現,甚至超過了 G1,接近了「吞吐量優先」的 Parallel 收集器組合,能夠說近乎實現了「魚與熊掌兼得」。
ZGC 的內存佈局
與 Shenandoah 和 G1 同樣,ZGC 也採用基於 Region 的堆內存佈局,但與它們不一樣的是, ZGC 的 Region 具備動態性,也就是能夠動態建立和銷燬,容量大小也是動態的,有大、中、小三類容量:
ZGC 內存佈局
與 Shenandoah 同樣,ZGC 在工做過程當中也幾乎是全程與用戶程序併發的,重點也是實現了標記-整理算法的整理階段能夠與用戶程序併發執行。可是兩者的實現方式不一樣,Shenandoah 是在對象身上添加轉發指針的方法,而 ZGC 則是直接在指針上動手腳,也就是傳說中的染色指針 (Colored Pointers),這個指針就是 Java 對象的引用,例如:
Object o = new Object();
其中「o」 只是一個引用,也就是指針,指向存在堆上的對象實例,引用自身也是要佔內存的,普通引用在32位機器佔4個字節,在64位機器上,開啓壓縮指針 (-XX:+UseCompressedOops) 的話佔4個字節,不開啓的話佔8個字節。ZGC 的染色指針結構以下 (不支持32位機器和壓縮指針):
染色指針結構示意圖
得益於染色指針上標誌位的支持,ZGC 也能夠像 Shenandoah 那樣,實現了在移動存活對象的過程當中能夠與用戶程序併發執行,且效率更高。ZGC 還用到了不少其餘的黑科技,原理過於複雜,就不在這裏詳述了。
在 JDK 11 及以上版本,能夠經過如下參數開啓 ZGC:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
上面介紹的各類收集器,好比 G一、Shenandoah 和 ZGC 等都是愈來愈複雜,愈來愈先進, 而 JDK 11 新加入的 Epsilon 倒是反其道而行,這款收集器不會作任何垃圾收集的操做,也許叫作「內存分配器」更加合適。雖然很奇葩,可是它仍是有用武之地的,好比愈來愈火的微服務領域,若是系統運行時間很短,在堆內存耗盡以前就能夠結束,那麼垃圾收集也就沒有任何意義了,這正是 Epsilon 的使用場景。
本文爲你們介紹了目前 HotSpot 虛擬機上的全部垃圾收集器,有的已經久經沙場,有的仍處於試驗階段,但有望在將來成爲主流,在實際應用中,你們能夠根據具體場景選擇合適的垃圾收集器。
參考資料: