垃圾收集器的整體概述
經典垃圾收集器之間的關系圖如下:
上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用,圖中收集器所在的區域,則表示它是屬于新生代收集器或是老年代收集器。
Serial收集器
Seria收集器是最基礎、最悠久的收集器。該收集器是一個單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會用一個處理器或者一個條收集線程去完成垃圾收集工作,更重要的是強調在它垃圾收集時,必須暫停其所有工作線程(Stop the World),直到它收集結束。Serial Old收集器的運行過程如下:
Serial收集器是客戶端模式下的默認新生代收集器,有著優于其他收集器的地方,就是簡單高效(與其他收集器的單線程相比),對于內存資源受限的環境,它是所有收集器里額外內存消耗最小的。對于單核處理器或者處理器核心數較少的環境來說Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。用戶桌面的應用場景分配給虛擬機的內存一般來說并不是很大,收集幾十兆甚至一兩百兆的新生代(僅僅指新生代使用的內存,桌面應用很少超過這個容量),收集器的停頓時間完全可以控制在十幾、幾十毫秒,最多一百毫秒以內,只要不是頻繁發生手機,這點停頓時間對許多用戶來說完全是可以接受的。所以Serial收集器對于運行在客戶端模式下的虛擬機來說是一個很好的選擇。
ParNew收集器
ParNew收集器實質上是Serial收集器的多線程并行版本,除了使用多條線程進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(如:-XX:SurvivorRatio、-XX:PretenureSizeThreShold、-XX:HandlePromotionFailure等)、收集算法、Stop the World 、對象分配規則、回收策略等都與Serial收集器完全一致。ParNew收集器的工作過程為:
ParNew收集器除了支持多線程并行收集器之外,其他與Serial收集器并沒有太多創新之處,但它是不少運行在服務端模式下的HotSpot虛擬機,尤其JDK7之前的遺留系統中首選的新生代收集器,其中有一個功能、性能無關但很重要的原因是:
除了Serial收集器外,目前只有ParNew收集器能與CMS收集器配合工作
。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款高性能的新生代收集器,它同樣是基于標記-復制算法實現的收集器,也是能夠并行收集的多線程收集器。Parallel Scavenge的諸多特性表面上看起來與ParNew收集器非常相似,那么它們之間的區別是什么呢?
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能的縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是處理器用于運行用戶代碼時間與處理器總耗時間的比值。即:吞吐量=運行用戶代碼時間/運行用戶代碼時間+運行垃圾收集時間
。Parallel Scavenge收集器運行過程如下:
如果虛擬機完成某項任務,用戶代碼加上垃圾收集總耗時100min,其中垃圾收集占用1min,那么吞吐就是99%。停頓時間越短就越適合需要與用戶交互或者需要保證服務響應質量的程序,良好的響應速度能提升用戶體驗;而高吞吐則可以最高效的利用處理器資源,盡快完成程序的運算任務,主要適合后臺運算而不需要太多交互的分析任務。
Parallel Scavenge收集器提供了兩個參數用于精確控制吞吐量,分別是控制 最大垃圾收集停頓時間的
-XX:MaxGCPauseMillis
參數以及直接設置吞吐量大小的-XX:GCTineRatio
參數。
-
-XX:MaxGCPauseMillis
是設置允許的值是一個大于0的毫秒數,收集器將盡量保證內存回收花費的時間不超過用戶的設定值。不過大家不要以為該值設置的越小能使得系統垃圾收集速度變快,垃圾收集停頓時間縮短是犧牲吞吐量和新生代空間為代價換取的:系統把新生代調小一些,收集300M新生代內存肯定比500M快,但也直接導致垃圾收集發生的更頻繁,原來10s收集一次,每次停頓時間100ms,現在為5s一次,每次停頓70ms。停頓時間在下降,但是吞吐量也降下來了。 -
-XX:GCTineRatio
參數設置在0到100之間的整數,也就是垃圾收集時間占總時間的比率,相當于吞吐量的倒數,譬如把此參數設置為19,那允許的最大垃圾收集時間就占總時間的5%即1/(1+19),默認值為99,即允許最大1%(即1/(1+99))的垃圾收集時間。
Parallel Scavenge收集器被稱為吞吐量優先收集器,除了上述中的參數設置外,還有一個參數-XX:+UseAdaptiveSizePolicy
,這是一個開關參數,當這個參數激活后,就不需要人工指定新生代大小、Eden與Survivor的大小比例(-XX:SurvivorRatio)、晉升老年代對象大?。?XX:PretenureSizeThreShold)等參數,虛擬機會自動根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。這種調節方式成為垃圾收集的自適應的調節策略(GC Ergonomics)。如果我們對收集器的運作不太了解,手工優化困難的話,使用Parallel Scavenge收集器配合自適應調整策略,把內存管理交給虛擬去完成也是不錯的選擇,我們只需要把基本的內存數據設置好(如-Xmx設置最大堆),然后使用-XX:MaxPauseMillis
參數(更關注最大停頓時間)或者-XX:GCTimeRatio(更關注吞吐量)參數非虛擬機設立一個優化目標,具體細節參數的調節工作由虛擬機完成。自適應調節策略也是Parallel Scavenge收集器區別于ParNew收集器的一個重要特性。
Serial Old收集器
Serial Old收集器是單線程老年代收集器,使用的是標記-整理算法。這個收集器的意義主要也是提供在客戶端模式下的HotSpot虛擬機使用。如果在服務端使用,它也可能有兩種用途:
- 一種是JDK1.5及之前的版本與Parallel Scavenge收集器搭配使用
- 另外一種就是作為CMS收集器發生失敗時的后備預案,在并發收集發生Concurrent Mode Failure時使用
Serial Old收集器的工作過程如下:
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多線程并發收集,基于標記-整理算法實現,在此收集器出現之前(JDK1.6之前)Parallel Scavenge收集器只能與Serial Old收集器搭配使用,直到Parallel Old收集器出現之后,”吞吐量優先“收集器有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場景下,可以優先考慮Parallel Scavenge收集器加Parallel Old收集器。Parallel Old收集器運行過程如下:
CMS收集器
CMS(Concurrent Mark Sweep)
收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網網站或者基于瀏覽器的B/S系統的服務上,這類應用通常都會較為關注服務的響應速度,希望系統停頓時間盡可能的短,以給用戶良好的交互體驗。
CMS收集器是基于標記-清除算法實現的,主要運作過程主要包含四個步驟:
- 初始標記(CMS inital mark):僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快
- 并發標記(CMS concurrent mark):從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時比較長不需要停頓用戶線程,用戶線程可以與垃圾收集線程一起并發運行。
- 重新標記(CMS remark):重新標記階段主要是為了修正并發標記,因用戶線程繼續運作而導致標記產生變化的那一部分對象的標記記錄(增量更新)。這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比并發標記階段的時間短。
- 并發清除(CMS concurrent sweep):清理刪除標記階段判斷已經死亡的對象,這個階段也是可以與用戶線程同時并發的。
初始標記階段和重新標記階段這兩個步驟任然需要Stop The World
。由于整個過程中耗時最長的并發標記和并發清除階段,都是垃圾收集線程和用戶線程一起工作,所以從總體來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。CMS收集器運行過程如下:
CMS是一款優秀的收集器,它的主要的優點已經在名字上體現出來:并發收集、低停頓。一些官網的公開文檔也稱之為“并發低停頓收集器”。但是CMS收集器也存在明顯的缺點:
- CMS收集器對處理器資源非常敏感,事實上,面向并發設計的程序都對處理器資源比較敏感。在并發階段,它雖然不會導致用戶線程停頓,但是卻會占用一部分線程)(或者處理器的計算能力)而導致應用程序變慢,減低總吞吐量。CMS默認啟動的回收線程數是(處理器核心數量+3)/4 ,也就是說,如果處理器核心數在四個及以上,并發回收時垃圾收集線程只占用不超過25%的處理器運算資源,并且會隨著處理器核心數量的增加而下降。但當處理器核心數量不足四個時,CMS對用戶程序的影響可能變得很大。如果應用本來的處理器負載很高,還要分出一半的運算能力去執行收集器線程,就可能導致用戶線程的執行速度忽然大幅降低。為了緩解這種情況,虛擬機提供了一種
“增量式并發收集器(Incrementak Concurrent Mark Sweep/i-CMS)”
的CMS收集器的變種。所做的事情和以前單核處理器年代PC機操作系統靠搶斷式多任務來模擬多核并行多任務的思想一樣,是在并發標記、清理的時候讓收集器線程、用戶線程交替執行,盡量減少垃圾收集線程的獨占資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得較少一些。直觀感受是速度變慢的時間更多了,但速度下降幅度就沒有那么明顯。實踐證明增量式的CMS收集器效果很一般,所以到JDK9發布后i-CMS模式被廢棄。 - CMS收集器無法處理
“浮動垃圾(Floating Garbage)”
,有可能出現“Con-currentMode Failure” 失敗進而導致空一次完全“Stop The Word”的Full GC的產生。在CMS的并發標記和并發清理階段,用戶線程是還在繼續運行的,程序在運行自然還會伴隨有新的垃圾對象的產生,但這一部分垃圾對象是出現在標記過程結束以后,CMS無法在當次收集中處理掉他們,之后留待下一次垃圾收集時再清理掉。這一部分就稱為“浮動垃圾”。同樣也是由于在垃圾收集階段用戶線程還需要持續運行,那就還需要預留足夠的空間提供給用戶線程使用,因此CMS收集器不能像其他垃圾收集器那樣等到老年代幾乎完全填滿了再進行收集,必須預留一部分空間供并發收集時的程序運作使用。在JDK5的默認設置下,CMS收集器當老年代使用了68%的空間后就會被激活,這是一個偏保守的設置,如果實際應用中老年代增長并不是太快,可以適當調高參數-XX:CMSInitiationOccu-pancyFraction
的值來提高CMS的觸發百分比,降低內存回收頻率,獲取更好的性能。到了JDK6,CMS收集器的啟動閾值就已經默認提升至92%。但這又會更容易面臨另一種風險:要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次“并發失敗(Concurrent Mode Failure)”,這時候虛擬機將不得不啟動后備預案:凍結執行線程,臨時啟用Serial Old收集器來重新進行老年代的額垃圾收集,但這樣停頓時間更長了。所以參數-XX:CMSInitiationOccupancyFraction
設置得太高將會容易導致大量的并發失敗產生,性能反而降低,用戶在生產中根據實際應用場景來權衡設置。 - CMS采用的是標記清楚算法,這就意味著收集結束時可能會存在大量的內存空間碎片??臻g碎片過多時,將會給大對象分配帶來很大麻煩,往往老年代還有很多剩余空間,但是無法找到足夠的連續空間來分配當前對象,而不得不提前觸發一次Full GC的情況。為了解決這個問題,CMS提供了一個參數
-XX:UseCMS-CompacttAtFullCollection
開關參數(默認開啟,從JDK1.9開始廢棄),用戶CMS收集器不得不進行Full GC時開啟內存碎片的合并整理過程,由于這個內存整理必須移動存活對象(Shenandoah和ZGC出現之前)是無法并發的。這樣碎片問題是解決了,但是停頓時間變長了。因此虛擬機設計者還提供了另外一個參數-XX:CMSFullGCBefore-Compaction
(此參數從JDK9開始廢棄),這個參數的主要作用是要求CMS收集器執行過若干次(數量由參數值決定)不整理空間的Full GC之后,下次進入Full GC前會先進行碎片整理(默認為0,表示每次進入Full GC時都進行碎片整理)。
G1收集器
G1是一款面向服務端的垃圾收集器。G1收集器它可以面向堆內任何部分來組成回收集(Collection Set,CSet)進行回收,衡量標準不再是它屬于哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。G1是基于Region的堆內存布局,雖然G1也仍遵循分代收集理論設計的,但堆內存的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續Java堆內存劃分為多個大小相等的對立區域(Region),每個Region都可以根據需要,扮演新生代Eden、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region采用不同的策略去處理,這樣無論是新創建的對象還是已存活了一段時間、熬過多次收集的就對象都是能夠獲取很好的手機效果。
Region中還有一類特殊的Humongous區域,專門用來存儲大對象
。G1人為只要大小超過了Region大小的一半的對象即可判定為大對象,每個Region的大小可以通過-XX:G1GeapRegionSize
設定,取值范圍為1~32M,且為2的N次冪。而對于超過了整個Region容量的超級大對象,將會被放在N個連續的Humongous Region中。G1大多數行位都把Humongous Region
作為老年代的一部分進行看待。G1劃分Region示意圖如下:
雖然G1仍然保留新生代和老年代的概念,但是新生代和來年代不再是固定的了,它們都是一系列區域(不需要連續)的動態集合。G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃圾堆積的”價值“大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然后在后臺維護一個優先列表,每次根據用戶設定允許的收集停頓時間(使用參數
-XX:MAxGCPauseMillis
指定,默認值是200ms),優先處理回收價值最大的那些Region(Garbage First->G1)。這樣使用Region劃分內存空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取盡可能高的收集效率。G1的實現的幾個關鍵節點介紹:
-
G1將堆空間劃分為多個獨立Region,Region里面存在跨Region引用對象如何解決? 使用記憶集避免全堆作為GC Roots掃描,但在G1收集器上
記憶集
的應用要復雜很多,每個Region都要維護自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針,并標記這些指針分別在哪些卡頁的范圍之內。G1的記憶集在存儲結構上實際是一中Hash表,Key是別的region的其實地址,value是一個集合,里面存儲的元素是卡表的索引號。這種”雙向“卡表接口(我指向了誰,誰指向了我)比原來的卡表實現更復雜,同時由于Region數量比傳統收集器的分代數量明顯要多的多,因此G1收集器比其他傳統垃圾收集器有更高的內存占用負擔(經驗值大約占用java堆空間的10%~20%)。 - 并發標記階段如何保證收集線程與用戶線程互不干擾地運行? 首先要解決用戶線程改變對象引用關系時,必須保證其不能打破原有對象圖結構,導致標記結果出現錯誤,該問題的解決辦法G1主要采用了原始快照(SATB)算法實現。此外垃圾收集對用戶線程的影響還體現在回收過程中創建對象的內存分配上,程序要繼續運行就可定會持續有新對象被創建,G1為每一個Region設計了兩個名為TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用于并發回收過程中的新對象分配,并發回收時新分配的對象地址必須要在這兩個指針位置以上。G1收集器默認在這個地址以上的對象都是被隱式標記過的,即默認他們是存活的,不在回收范圍之內。如果內存回收速度趕不上內存分配速度,G1被迫使用Serial Old 進行Full GC而產生Stop the World。
-
怎樣建立可靠停頓預測模型? 用戶通過
-XX: MaxGcPauseMillis
參數指定停頓時間只意味著垃圾收集發生之前的期望值,G1收集器的停頓預測模型是以衰減值(Decaying Average)為理論基礎來實現,在垃圾收集過程中,G1收集器會記錄每個Region的回收耗時,每個Region記憶集里的臟卡數量等各個可測量的步驟花費的成本,并分析得出平均值、標準偏差、置信度等統計信息。這里強調的”衰減平均值“是指它會比普通的平均值更容易受到新數據的影響,平均值代表整體平均狀態,但衰減平均值更準確地代表”最近的“平均狀態。換句話說,Region的統計狀態越新越能決定其回收的價值。然后通過這些信息預測現在開始回收的話,由哪些Region組成回收集才可以在不超過預期挺短時間的約束下獲得最高的收益。
G1收集器運作過程主要可劃分為以下四個步驟:
-
初始標記
:僅僅標記GC Roots直接能關聯的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發運行時,能正確的在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器這個階段并沒有額外的停頓。 -
并發標記
:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆了的對象圖,找出要回收的對象,這個階段耗時比較長,但可與用戶線程并發執行。當對象圖掃描完成以后,還要重新處理SATB記錄下的在并發時有引用變動的對象。 -
最終標記
:對用戶線程做另一個短暫的停頓,用戶處理并發階段結束后扔遺留下來的最后少量的SATA記錄。 -
篩選回收
:負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收那一部分Region的存活對象復制到空的Region中,在清理整個舊Region空間。這里的操作涉及存活對象的移動,必須暫停用戶線程,由多條收集器線程并行完成。
G1運行過程示意圖:
CMS與G1比較:
- CMS采用標記-清理算法產生大量的內存分片問題,G1采用標記-復制算法,垃圾收集完成后能夠提供規整的可用內存,這種特性有利于程序的長時間運行,在程序為大對象分配內存時不容易因無法找到連續的內存空間而出發下一次收集。
- G1因為每個region必須維護一份卡表(記錄跨region的引用關系),這將會導致G1的記憶集可能會占用整個堆容量的更大的內存空間;相比CMS卡表只要一份只需要記錄老年代到新生代的引用。
- G1和CMS各自的細節實現導致了用戶程序運行時的負載會不同。CMS使用的是寫屏障來維護卡表,G1除了使用寫后屏障來維護卡表操作外,為了實現原始快照搜索(SATB)算法,還需要寫前屏障來跟蹤并發時的指針變化情況。比起增量更新算法,原始快照搜索能夠減少并發標記和重新標記階段的消耗,避免CMS在最終標記階段停頓過長的缺點,但在用戶程序運行過程中確實會產生由跟蹤引用變化帶來的額外負擔。