解析JDK 7的Garbage-First收集器

Garbage-First(後文簡稱G1)收集器是當今收集器技術發展的最前沿成果,在Sun公司給出的JDK RoadMap裏面,它被視做JDK 7的HotSpot VM 的一項重要進化特徵。從JDK 6u14中開始就有Early Access版本的G1收集器供開發人員實驗、試用,雖然在JDK 7正式版發佈時,G1收集器仍然沒有擺脫「Experimental」的標籤,可是相信不久後將會有一個成熟的商用版本跟隨某個JDK 7的更新包發佈出來。算法

因版面篇幅限制,筆者行文過程當中假設讀者對HotSpot其餘收集器(例如CMS)及相關JVM內存模型已有基本的瞭解,涉及到基礎概念時,沒有再延伸介紹,讀者可參考相關資料。數據庫

G1收集器的特色

G1是一款面向服務端應用的垃圾收集器,Sun(Oracle)賦予它的使命是(在比較長期的)將來能夠替換掉JDK 5中發佈的CMS(Concurrent Mark Sweep)收集器,與其餘GC收集器相比,G1具有以下特色:服務器

  • 並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。併發

  • 分代收集:與其餘收集器同樣,分代概念在G1中依然得以保留。雖然G1能夠不需其餘收集器配合就能獨立管理整個GC堆,但它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象以獲取更好的收集效果。佈局

  • 空間整合:與CMS的「標記-清理」算法不一樣,G1從總體看來是基於「標記-整理」算法實現的收集器,從局部(兩個Region之間)上看是基於「複製」算法實現,不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC。性能

  • 可預測的停頓:這是G1相對於CMS的另一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器特徵了。測試

實現思路

在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局與就與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。spa

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內獲能夠獲取儘量高的收集效率。操作系統

G1把內存「化整爲零」的思路,理解起來彷佛很容易理解,但其中的實現細節卻遠遠沒有現象中簡單,不然也不會從04年Sun實驗室發表第一篇G1的論文拖至今將近8年時間都尚未開發出G1的商用版。筆者舉個一個細節爲例:把Java堆分爲多個Region後,垃圾收集是否就真的能以Region爲單位進行了?聽起來瓜熟蒂落,再仔細想一想就很容易發現問題所在:Region不多是孤立的。一個對象分配在某個Region中,它並不是只能被本Region中的其餘對象引用,而是能夠與整個Java堆任意的對象發生引用關係。那在作可達性斷定肯定對象是否存活的時候,豈不是還得掃描整個Java堆才能保障準確性?這個問題其實並不是在G1中才有,只是在G1中更加突出了而已。在之前的分代收集中,新生代的規模通常都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象也面臨過相同的問題,若是回收新生代時也不得不一樣時掃描老年代的話,Minor GC的效率可能降低很多。。線程

在G1收集器中Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查引是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。

運做過程

若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:

  • 初始標記(Initial Marking)

  • 併發標記(Concurrent Marking)

  • 最終標記(Final Marking)

  • 篩選回收(Live Data Counting and Evacuation)

對CMS收集器運做過程熟悉的讀者,必定已經發現G1的前幾個步驟的運做過程和CMS有不少類似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象,這階段須要停頓線程,但耗時很短。併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。而最終標記階段則是爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行。最後篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃,從Sun透露出來的信息來看,這個階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。經過圖1能夠比較清楚地看到G1收集器的運做步驟中併發和須要停頓的階段。

圖1 G1收集器運行示意圖

G1收集器的實際性能

因爲目前尚未成熟的版本,G1收集器幾乎能夠說尚未通過實際應用的考驗,網上關於G1收集器的性能測試很是貧乏,筆者沒有Google到有關的生產環境下的性能測試報告。強調「生產環境下的測試報告」是由於對於垃圾收集器來講,僅僅經過簡單的Java代碼寫個Microbenchmark程序來建立、移除Java對象,再用-XX:+PrintGCDetails等參數來查看GC日誌是很難作到準衡量其性能的(爲什麼Microbenchmark的測試結果不許確可參見筆者這篇博客:http://icyfenix.iteye.com/blog/1110279)。所以關於G1收集器的性能部分,筆者引用了Sun實驗室的論文《Garbage-First Garbage Collection》其中一段測試數據,以及一段在StackOverfall.com上同行們對G1在真實生產環境下的性能分享討論。

Sun給出的Benchmark的執行硬件爲Sun V880服務器(8×750MHz UltraSPARC III CPU、32G內存、Solaris 10操做系統)。執行軟件有兩個,分別爲SPECjbb(模擬商業數據庫應用,堆中存活對象約爲165MB,結果反映吐量和最長事務處理時間)和telco(模擬電話應答服務應用,堆中存活對象約爲100MB,結果反映系統能支持的最大吞吐量)。爲了便於對比,還收集了一組使用ParNew+CMS收集器的測試數據。全部測試都配置爲與CPU數量相同的8條GC線程。

在反應停頓時間的軟實時目標(Soft Real-Time Goal)測試中,橫向是兩個測試軟件的時間片斷配置,單位是毫秒,以(X/Y)的形式表示,表明在Y毫秒內最大容許GC時間爲X毫秒(對於CMS收集器,沒法直接指定這個目標,經過調整分代大小的方式大體模擬)。縱向是兩個軟件在對應配置和不一樣的Java堆容量下的測試結果,V%、avgV%和wV%分別表明的含義爲:

  • V%:表示測試過程當中,軟實時目標失敗的機率,軟實時目標失敗即某個時間片斷中實際GC時間超過了容許的最大GC時間。

  • avgV%:表示在全部實際GC時間超標的時間片斷裏,實際GC時間超過最大GC時間的平均百分比(實際GC時間減去容許最大GC時間,再除以總時間片斷)。

  • wV%:表示在測試結果最差的時間片斷裏,實際GC時間佔用執行時間的百分比。


從上面結果可見,對於telco來講,軟實時目標失敗的機率控制在0.5%~0.7%之間,SPECjbb就要差一些,但也控制在2%~5%之間,機率隨着(X/Y)的比值減少而增長。另外一方面,失敗時超出容許GC時間的比值隨着總時間片斷增長而變小(分母變大了嘛),在(100/200)、512MB的配置下,G1收集器出現了某些時間片斷下100%時間在進行GC的最壞狀況。而相比之下,CMS收集器的測試結果對比之下就要差不少,3種Java堆容量下都出現了100%時間進行GC的狀況,

在吞吐量測試中,測試數據取3次SPECjbb和15次telco的平均結果。在SPECjbb的應用下,各類配置下的G1收集器表現出了一致的行爲,吞吐量看起來只與容許最大GC時間成正比關係,而在telco的應用中,不一樣配置對吞吐量的影響則顯得很微弱。與CMS收集器的吞吐量對比能夠看到,在SPECjbb測試中,在堆容量超過768M時,CMS收集器有5%~10%的優點,而在telco測試中CMS的優點則要小一些,只有3%~4%左右。

圖2:吞吐量測試結果

在更大規模的生產環境下,筆者引用一段在StackOverfall.com上看到的經驗分享:「我在一個真實的、較大規模的應用程序中使用過G1:大約分配有60~70GB內存,存活對象大約在20~50GB之間。服務器運行Linux操做系統,JDK版本爲6u22。G1與PS/PS Old相比,最大的好處是停頓時間更加可控、可預測,若是我在PS中設置一個很低的最大容許GC時間,譬如指望50毫秒內完成GC(-XX:MaxGCPauseMillis=50),但在65GB的Java堆下有可能獲得的直接結果是一次長達30秒至2分鐘的漫長的Stop-The-World過程;而G1與CMS相比,它們都立足於低停頓時間,CMS仍然是我如今的選擇,可是隨着Oracle對G1 的持續改進,我相信G1會是最終的勝利者。若是你如今採用的收集器沒有出現問題,那就沒有任何理由如今去選擇G1,若是你的應用追求低停頓,那G1如今已經能夠做爲一個可嘗試的選擇,若是你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處。」

在這節筆者引了兩段別人的測試結果、經驗後,對於G1給出一個本身的建議:直到如今爲止尚未一款「最好的」收集器出現,更加沒有「萬能的」收集器,因此咱們選擇的只是對具體應用最合適的收集器。對於不一樣的硬件環境、不一樣的軟件應用、不一樣的參數配置、不一樣的調優目標都會對調優時的收集器選擇產生影響,選擇適合的收集器,除了理論和別人的數據經驗做爲指導外,最終仍是應當創建在本身應用的實際測試之上,別人的測試,大可抱着「至於你信不信,反正我本身沒測以前是不信的」的態度。

相關文章
相關標籤/搜索