JVM有哪些垃圾回收器

JVM 的垃圾回收器

[TOC]

經典垃圾收集器

 如果說收集演算法是記憶體回收的方法論,那垃圾收集器就是記憶體回收的實踐者。
 這些經典的收集器儘管已經算不上是最先進的技術,但他們曾在實踐中千錘百鍊,足夠成熟。

HotSpot虛擬機的垃圾收集器
HotSpot虛擬機的垃圾收集器

 圖中展示了其中作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。圖中收集器所處的區域,則表示它是屬於新生代收集器抑或是老年代收集器。
 ps:雖然垃圾收集器的技術在不斷進步,但直到現在還沒有最好的收集器出現,更加不存在"萬能"的收集器,所以我們選擇的只是對具體應用最合適的收集器。如果有一种放之四海而皆準、任何場景下都適用的完美收集器存在。HotSpot虛擬機完全沒必要實現那麼多種不同的收集器了

Serial 收集器

       Serial 收集器是最基礎、歷史最悠久的收集器,曾經(JDK 1.3.1之前)是HotSpot虛擬機新生代收集器的唯一選擇。從名字就可以猜出,這個收集器是一個**單執行緒**工作的收集器。 但它的 "單執行緒"的意義並不僅僅是說明它只會使用一個處理器
 或一條收集執行緒去完成垃圾收集工作,更重要的是強調它在進行垃圾收集時,必須暫停其他所有工作執行緒,直到它收集結束。"Stop The World" 這個詞語也許聽起來很酷,但這項工作是由*虛擬機在後台自動發起和自動完成的*,這對很多應用來說都是不能接受
 的,不妨試想一下,要是你的電腦每運行一小時就會暫停五分鐘,你會有什麼心情?

Serial/Serial Old 收集器運行示意圖
Serial/Serial Old 收集器運行示意圖

       雖然 Serial 收集器最早出現,但目前已經老而無用,食之無味,棄之可惜的"雞肋",但事實上,它仍然時HotSpot虛擬機運行在客戶端模式下的默認新生代收集器有著優於其他收集器的地方,那就是簡單而高效,對於記憶體資源受限的環境,他是所有收集器里
 額外記憶體消耗最小的;對於單核處理器或處理器核心數較少的環境來說,Serial收集器由於沒有執行緒交互的開銷,專心做垃圾收集器自然可以獲得最高的單執行緒收集效率。在用戶桌面的應用場景以及近年來流行的部分微服務應用中,分配給虛擬機管理的記憶體一般來說
 並不會特別大,收集幾十兆甚至一二百兆的新生代,垃圾收集器的停頓時間完全可以控制在十幾,幾十毫秒,最多一百多毫秒以內,只要不是頻繁發生收集,這點停頓時間對許多用戶來說完全是可以接受的。

ParNew 收集器

       ParNew 收集器實質上時Serial收集器的多執行緒並行版本,除了同時使用多條執行緒進行垃圾收集之外,其餘的行為包括 Serial收集器可用的所有控制參數(例如 -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集演算法
 Stop The World、對象分配規則、回收策略等都完全一致。

ParNew/Serial Old收集器運行示意圖
ParNew/Serial Old收集器運行示意圖

       ParNew 收集器除了支援多執行緒並行收集之外,其他於Serial收集器相比並沒有太多創新之處,但它確實不少運行在服務端模式下的HotSpot虛擬機,時JDK 7 之前遺留系統首選的新生代收集器,其中有一個與功能、性能無關但其實
 很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器配合工作
       ps:JDK 9 開始,ParNew加CMS收集器的組合就不再是官方推薦的服務端模式下的收集器解決方案了。官方細外它能夠完全被 G1 所取代甚至還取消了ParNew 加 Serial Old 以及 Serial 加 CMS 這兩組收集器組合的支援。
 也可以理解為ParNew 合併入CMS成為它專門處理新生代的組成部分。
       -XX:ParallelGCThreads 參數來限制垃圾收集的執行緒數

Parallel Scavenge 收集器

       Parallel Scavenge 收集器也是一款新生代收集器,它同樣是基於標記-複製演算法實現的收集器,也是能夠並行收集的多執行緒收集器。
       Parallel Scavenge 收集器的特點是它的關注點與其他收集器不同,CMS 等收集器的關注點是儘可能地縮短垃圾收集時用戶執行緒地停頓時間,而PS 收集器地目標則時達到一個可控地吞吐量(Throughput)。所謂吞吐量就是處理器用於運行用戶程式碼的時
 間與處理器總消耗時間地比值  即 吞吐量 = (運行用戶程式碼時間)/(運行用戶程式碼時間 + 運行垃圾收集時間) 
       如果虛擬機完成某個任務,用戶程式碼加垃圾收集總耗費了100分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99% 。停頓時間越短就越適合需要與用戶交互或需要保證服務響應品質的程式,良好的響應速度能提高用戶體驗;而高吞吐量則可以最高效率
 地利用處理器資源,儘快完成程式地運算任務。主要適合在後台運算而不需要太多交互地分析任務。
       Paraller Scavenge 收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis 參數以及直接設置吞吐量大小的 -XX:GCTimeRatio參數
             1.-XX:MaxGCPauseMillis 參數允許的值是一個大於0的毫秒數,收集器將儘力保證記憶體回收花費的時間不超過用戶設置值。不過不要異想天開的認為如果把這個參數值設置的更小就可以使得系統垃圾收集速度變得更快,垃圾收集停頓時間
 是以犧牲吞吐量和新生代空間為代價換取的。
             2.-XX:GCTimeRatio 參數的值應時一個大於0 小於100 的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。譬如把此參數設置為 19 ,那允許的最大垃圾收集時間就佔總時間的 5 % (即 1/(1+19)),默認值為99。
 即允許最大 1%(1/(1+99)) 的垃圾收集時間。
             3.Parallel Scavenge 收集器也經常被稱作"吞吐量優先收集器",還有一個參數 -XX:UseAdaptiveSizePolicy 這是一個開關參數,激活之後就不需要人工指定新生代的大小(-Xmn)、Eden 與Survivor區的比例(-XX:SurvivorRatio)、
 晉陞老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了。虛擬機會根據當前系統的運行情況收集性能監控資訊、動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。這種調節方式稱為垃圾手機的自適應調節策略(GC Ergonomics)。
       只需要把基本的記憶體數據設置號(-Xmx最大堆)然後使用1,2 參數給虛擬機設置一個優化目標,那具體細節參數調節工作就有虛擬機完成。自適應調節也是 Parallel Scavenge收集器區別於 ParNew 收集器的一個重要特徵。

Serial Old 收集器

       Serial Old 時Serial收集器的老年代版本,它同樣是一個單執行緒收集器,<font  color="red">使用標記-整理演算法。</font> 
 這個收集器的主要意義也是共客戶端模式下的HotSpot虛擬機使用,
 如果在服務端模式下,它也可能有兩種用途:
       1.在JDK 5 以及以前的版本種與 Parallel Scavenge收集器搭配使用。
       2.作為 CMS 收集器發生失敗時的後備預案,在並發收集發生 Concurrent Mode Failure時使用。

Parallel Old 收集器

       Parallel Old 時Parallel Scavenge 收集器的老年代版本,支援多執行緒並發收集,基於 **標記-整理演算法** 實現。這個收集器時直到 JDK 6 時才開始提供的,"吞吐量優先"收集器終於有了比較名副其實的搭配組合。

CMS 收集器

簡述及運行過程

       CMS (Concurrent Mark Sweep)收集器是一種 **以獲取最短回收停頓時間** 為目標的收集器。非常適用於關注服務的響應時間,細外系統停頓時間儘可能短,以給用戶帶來很好的交互。
 基於 **標記-清除** 演算法實現 整體過程分為四個步驟包括
       1.初始標記(CMS initial mark)   (STW) 僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快
       2.並發標記(CMS concurrent mark)  從直接關聯對象開始遍歷整個對象圖的過程,耗時較長但是不需要停頓用戶執行緒
       3.重新標記(CMS remark)            (STW)是為了修正並發標記期間,因用戶執行緒繼續運行而導致標記產生的那一部分對象的標記記錄
       4.並發清除(CMS concurrent sweep) 清理刪除標記階段判斷的已經死亡的對象

CMS 收集器運行示意圖
CMS 收集器運行示意圖

CMS 優點及其缺點

優點
       CMS 是一款優秀的收集器,它最主要的優點在名字上已經體現出來:**並發收集、低停頓**
缺點
       CMS 收集器時HotSpot虛擬機追求低停頓的第一次成功嘗試,但是它還遠達不到完美的程度,至少有以下三個明顯的缺點:
CMS 對處理器資源非常敏感
       事實上,面向並發設計的程式都對處理器資源比較敏感,在並發階段它雖然不會導致用戶執行緒停頓,但卻會因為佔用了一部分執行緒而導致應用程式變慢,降低總吞吐量。CMS 默認啟動的回收執行緒數是(處理器核心數量+3)/4,也就是說,如果處理器核心數
 在四個或以上,並發回收時垃圾收集執行緒只不過佔用不超過25% 的處理器運算資源,並且會隨處理器核心數量的增加而下降。但當處理器核心數量不足四個時,CMS 對用戶程式的影響就肯恩變得很大。未來緩解這種情況,虛擬機提供了一種稱為「增量式並
 發收集器」的CMS收集器變種,在並發標記、清理的時候讓收集器執行緒、用戶執行緒交替運行,盡量減少垃圾收集執行緒的獨佔資源的時間。JDK 7 i-CMS模式已經被聲明為"deprecated" JDK 9 被完全廢棄
CMS 無法處理”浮動垃圾” 及 需要注意 CMS觸發時間以及發生”並發失敗”
       在CMS 的並發標記和並發清理階段,用戶執行緒還在繼續運行的,程式在運行過程自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以後,CMS 無法在當此收集種處理掉它們,只要留待下一次垃圾收集時再清理掉。
 這一部分垃圾就稱為"浮動垃圾"。同樣也是由於在垃圾收集階段用戶執行緒還需要持續運行,那就還需要預留足夠記憶體空間提供給用戶執行緒使用,因此 CMS 收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿在進行收集,必須預留一部分空間供並發
 收集時的程式運作使用。
       JDK 5 的默認設置下,當老年代使用 68% 的空間後就會被激活。可以適當調用參數 -XX:CMSInitiatingOccu-pancyFraction 的值提高 CMS 出發百分比降低記憶體回收頻率,獲取更好的性能,到JDK 6時 CMS 啟動閾值已經默認提升至 92% ,但
 這又會更容易面臨另一種風險,要是 CMS 運行期間預留的記憶體無法滿足程式分配新對象的需要,就會出現一次 "並發失敗"(Concurrent Mode Failure)這時候虛擬機將不得不啟動後備預案:凍結用戶執行緒的執行,臨時啟用Serial Old 收集器來重新進行
 老年代的垃圾收集,但這樣停頓時間就很長了,所有上述參數設置太大將會很容易導致大量的並發失敗產生,性能反而降低。
採用”標記-清除”演算法造成空間碎片過多 造成大對象分配問題
       使用該演算法意味著手機結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很多剩餘空間,但就是無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次 Full FC 的情況。為了解決
 這個問題,CMS 收集器提供了一個 -XX:UseCMSCompactAtFullCollection 開關參數(默認開始 JDK9 開始廢棄),用於在CMS收集器不得不進行Full GC時開啟記憶體碎片的合併整理過程,由於記憶體整理必須移動存活對象,是無法並發的。這樣空間碎片的問
 題是解決了但停頓時間又會變長,因此還有一個參數 -XX:CMSFullGCsBeforeCompaction(默認值為0,每次進入Full GC 都進行碎片整理 ,JDK9 開始廢棄),參數要求CMS收集器在執行若干次不整理空間的 Full GC後,下一次進入FullGC就會先進行碎
 片整理。

Garbage First 收集器

簡述

       Garbage First(簡稱G1)收集器時垃圾收集器技術發展歷史上里程碑式的成果,它開創了收集器面向局部收集的設計思路和基於Region的記憶體布局形態,JDK 8 Update 40 的時候,G1提供並發的類卸載的支援,補全了其計劃功能的最後一塊拼圖。
 這個版本的 G1 收集器才被Oracle 官方稱為"全功能的垃圾收集器"(Full-Featured Garbage Collector)。
       G1 是一款主要面向**服務端應用**的垃圾收集器。JDK 9 發布之日,G1宣告取代Parallel Scavenge加Parallel Old 組合,成為服務端模式下的默認垃圾收集器,而CMS 則淪落至被聲明偉不推薦使用(Deprecate)的收集器。

運行過程

       1.初始標記: 僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS指針的值,讓下一階段用戶執行緒並發運行時,能正確地在可以用Region中分配新對象。這一階段需要停頓執行緒,但耗時很短,而且是借用進行Minor GC的時候同步完成的,
 所有G1 收集器在這個階段實際並沒有額外的停頓
       2.並發標記: 從GC Roots開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找到要回收的對象,這階段耗時較長,但可與用戶程式並發執行。當對象圖掃描完成以後,還要重新處理SATB記錄下的在並發時有引用變動的對象。
       3.最終標記: 對用戶執行緒做另一個短暫的暫停,用於處理並發階段結束後仍遺留下來的最後那少量的SATB記錄。
       4.篩選回收: 負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來指定回收計劃,可以自由選擇任意多個Region構成會收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再
 清理掉整箇舊Region的全部空間。這裡的操作涉及存活對象的移動,時必須暫停用戶執行緒,由多條收集器執行緒並行完成的。

G1 收集器運行示意圖

停頓時間模型

       作為CMS收集器的替代者和繼承人,設計者們希望做出一款能夠建立起"停頓時間模型"(Pause Prediction Mdel)的收集器,停頓時間模型的意思時能夠支援指定在一個長度為 M毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過N毫秒這樣
 的目標。那具體如何實現這個目標呢?首先要有一個思想上的改變,在G1 收集器出現之前的所有收集器,包括CMS在內,垃圾收集器的目標範圍要麼是整個新生代(Minor GC),要麼就是整個老年代(Major GC),在要麼就是整個Java堆(Full GC)。而G1跳出
 這個樊籠,它可以面向堆記憶體任何部分來組成會收集(Collection Set 一般簡稱CSet)進行回收。衡量標準不再是它屬於哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。
Region
       G1 開創了基於Region 的堆記憶體布局是它能夠實現這一目標的關鍵。雖然G1也仍遵循分代收集理論設計的。但其堆記憶體的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續額Java堆劃分為多個大小
 相等的獨立區域(Region),每個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是以及存活了一段時間、熬過多次收集的就對象>
 都能獲取很好的收集效果。
Humongous(存儲大對象)
       Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為只要大小超過一個Region容量的一半的對象即可判斷為大對象。每個Region的大小可以通過參數 -XX:G1HeapRegionSize設置,取值範圍為1MB~32MB,且應為2的N次冪。
 而面對那些超過整個Region容量的超級大對象,將會被存放在N個連續的HumongousRegion之中,G1中大多數行為都把Humongous Region作為老年代的一部分看待。

G1收集器Region示意圖
G1收集器Region示意圖

-XX:MaxGcPauseMillis
       G1雖然任然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區域的動態集合。G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的記憶體空間都是Region大小的
 整數倍,這樣可以有計劃地避免在整個Java堆中盡心全區域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region裡面的垃圾堆積的"價值"大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後台維護一個優先順序列表,每次根據
 用戶設置的允許的收集停頓時間(參數-XX:MaxGCPauseMillis指定,默認200毫秒),優先處理回收價值收益最大的那些Region,也就是"Garbage First"名字的由來。---這種使用Region劃分記憶體空間,以及具有優先順序的區域回收方式,保證了G1收集器在
 有限時間內獲取儘可能高的收集效率。

G1 潛在問題

如何解決多Region存在的跨Region引用
       解決思路使用  [記憶集](//www.cnblogs.com/huan30/p/14323091.html)  避免全堆作為 GC Roots掃描,在G1收集器上記憶集的應用其實要複雜很多,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的
 指針並標記這些指針分別在那些卡頁的範圍之內。
       G1的記憶集在存儲結構的本質上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,理論存儲的元素是卡表的索引號。這種"雙向"的卡表結構(卡表是"我指向誰",這種結構還記錄著"誰指向我")比原來的卡表實現起來更複雜,同時由於Region
 數量比傳統收集器的分代數量明顯要多得多,因此G1收集器要比其他傳統垃圾收集器有著更高的記憶體佔用負擔。根據經驗,G1至少要消耗大約相當於Java堆容量10%至20%的額外記憶體來維持收集器工作
並發標記階段如何保證收集執行緒與用戶執行緒互不干擾
       這裡首先要解決的是用戶執行緒改變對象引用關係時,必須保證其不能打破原本的對象圖結構:CMS 收集器次啊用增量更新演算法實現,而G1收集器則是通過原始快照(STAB)演算法來實現。此外,垃圾收集堆用戶執行緒的影響還體現在回收過程中新創建對象的
 記憶體分配上,程式要繼續運行就肯定會持續有新對象被創建,G1為每一個Region涉及了兩個名為TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用於並發回收過程中新對象分配,並發回收時新分配的對象地址都必須要在這兩個指針位置
 以上。G1收集器默認在這個地址上的對象是被隱式標記過的,即默認它們是存活的,不納入回收範圍。與CMS中"Concurrent Mode Failure"失敗會導致Full GC 類似,如果記憶體回收的速度趕不上記憶體分配的速度,G1收集器也要被迫凍結用戶執行緒執行,導致
 Full GC而產生長時間"Stop The World"
怎麼建立起可靠的停頓預測模型
       用戶通過-XX:MaxGCPauseMillis參數指定的停頓時間只意味著垃圾收集發生之前的期望值,但G1收集器要怎麼做才能滿足用戶的期望呢?G1收集器的停頓預測模型是以衰減均值(Decaying Average)為理論基礎來實現的,在垃圾收集過程中,G1收集器
 會記錄每個Region的回收耗時、每個Region記憶集里的臟卡數量等各個可測量的步驟花費的成本,並分析得出平均值、標準偏差、置信度等統計資訊。話句話說,Region的統計狀態越新越能決定其回收的價值。然後通過這些資訊預測現在開始回收的話,有哪些
 Region組成的回收集才可以在不超過期望停頓時間的約束下獲得最高的收益。

G1 對比 CMS

優點
       指定最大停頓時間、分Region的記憶體布局、按收益動態確定回收集
       G1 整體採用 "標記-整理" 演算法實現,運行期間不會產生記憶體空間碎片。垃圾收集完成之後能提供規整的可用記憶體。有利於程式長時間運行,在程式為大對象分配記憶體時不容易因無法找到連續記憶體空間而提前出發下一次收集
缺點
       G1無論是為了垃圾收集產生的記憶體佔用還是程式運行的額外執行負載都要比CMS要高
記憶體佔用
       G1 和 CMS 都是用卡表來處理跨代指針,但G1的卡表實現更為複雜,而且堆中每個Region,無論扮演老年代還是新生代,都必須有一份卡表,這導致G1的記憶集可能會佔用整個堆容量的20%乃至更多的記憶體空間;相比起來CMS的卡表就相當簡單,只有唯一一份。
 而且只需要處理老年代到新生代的引用,反過來則不需要。
執行負載
       由於兩個收集器各自的細節實現特點導致了用戶程式運行時的負載會有不同,譬如:
 它們都是用到了寫屏障,CMS用寫後屏障來更新維護卡表;而G1除了使用寫後屏障來進行同樣的卡表維護操作外,為了實現原始快照搜索演算法,還需要使用寫前屏障來跟蹤並發時的指針變化情況。

低延遲垃圾收集器

       衡量垃圾收集器的三項最重要的指標是:記憶體佔用(Footprint)、吞吐量(Throughput)和延遲(Latency),三者最多可以同時達成其中的兩項。

Shenandoah 收集器

相比G1 的改進

       1.支援並發的整理演算法
       2.沒有使用分代收集
       3.使用"連接矩陣"來記錄跨Region的引用關係
運行過程九階段
       初始標記: 首先標記與GC Root是直接關聯的對象
       並發標記: 遍歷對象圖,標記出全部可達的對象
       最終標記: 處理剩餘SATB掃描,並在這個階段統計回收價值最高的Region,構成一組回收集
       並發清理: 用於清理那些整個區域內連一個存活對象都沒有找到的Region
       並發回收: 將回收集裡面的存活對象複製一份到其他未被使用的Region中。使用讀屏障和轉髮指針應對並發問題
       初始引用更新: 並發回收結束後把堆中所有指向舊對象的引用修正到複製後的新地址。實際並未做什麼,只是為了建立一個執行緒集合點確保上步任務完成
       並發引用更新: 真正開始進行引用更新操作。
       最終引用更新: 修正GC Roots中的引用
       並發清理: 整個回收集中所有Region再無存活對象,直接清空以便之後使用

Brooks Pointer 實現對象移動與用戶程式並發的一種解決方案

ZGC 收集器

特點

記憶體布局
       ZGC的Region具有動態性---動態創建和銷毀,以及動態的區域容量大小。
       小型Region: 容量固定為2MB,用於放置小於256KB的小對象
       中型Region: 容量固定32MB,用於放置大於等於256KB但小於4MB的對象
       大型Region: 容量不固定,可以動態變化,但必須是2MB的整數倍,用於放置4MB或以上的大對象。每個大型Region中只會存放一個大對象。因為複製一個大對象的代價非常高昂,所有不會被重分配,
並發整理演算法的實現
染色指針

染色指針

運行過程

       並發標記: ZGC的標記是在指針上而不是在對象上進行的,標記階段會更新染色指針中的Marked 0、Marked 1 標誌位。
       並發預備重分配: 這個階段需要根據特定的查詢條件統計得出本次收集過程中要清理那些Region將這些Region組成重分配集。
       並發重分配: 把重分配集中的存活對象複製到新的Region上,並為重分配集中的每個Region維護一個轉發表,記錄從就對象到新對象的轉向關係。
       並發重映射: 修正整個堆中指向重分配集中舊對象的所有引用。
Tags: