壹篇文章搞懂G1收集器
作為CMS收集器的替代者和繼承人,設計者們希望做出壹款能夠建立起“停頓時間模型”的收集器,停頓時間模型的意思是能夠支持指定在壹個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過N秒這樣的目標。
G1的裏程碑意義來源於其面向局部收集的設計思路和基於Region的內存布局形式,這也是G1實現其停頓時間模型的底氣。在G1收集器出現之前的所有其他收集器,包括被它所替代的CMS,垃圾收集的目標範圍都是整個新生代(Minor GC)或整個老年代(Major GC),亦或者是整個Java堆(Full GC)。而G1實現了可以面向堆內存的任何部分來組成回收集。衡量標準不再是分代,而是回收的實際收益,這就是Mixed GC模式。
G1基於Region的堆內存布局是它實現Mixed GC的關鍵。我們不能說G1擯棄了分代理論,相反,G1依然是依據分代理論設計的,但其堆內存布局與其他收集器有非常明顯的差異,它不再堅持固定大小以及固定數量的分代區域劃分,而是把堆分成多個大小相等的獨立區域,稱為Region,而每個Region都可能是新生代或老年代。這樣無論是針對哪種對象,都可以有比較好的收集效果。
從上圖中我們可以看見,Region中還有壹種Humongous區域,它專門用來存儲大對象。G1認為壹個對象的大小超過了壹個Region的壹半,那就可以稱為大對象。如果對象大小超過壹個Region,就存儲在連續的多個Region當中。另外值得註意的是,G1的大部分行為都把Humongous Region作為老年代的壹部分來看待。
G1之所以能夠建立起可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元。G1收集器會跟蹤每個Region中垃圾總的“價值”大小,即回收所獲得的空間大小和回收所需時間的經驗值,然後在後臺維護壹個優先級列表。並可以根據用戶的設定回收價值收益最大的Region。這也是“Garbage First”其名的由來。
G1收集器作為壹款跨時代的收集器,它從發表論文到商用經歷了超過十年的研發,其中解決了無數的問題,以下是三個典型且重要的問題向讀者說明。
和其他收集器解決跨代問題的方法壹樣,G1使用記憶集從而避免全堆作為GC Roots掃描。但不同的是G1的每個Region都維護屬於自己的記憶集,它們會記錄下別的Region指向自己的指針,並標記這些指針分別在哪些卡頁的範圍內。G1的記憶集在存儲結構的本質上是哈希表(key是別的Region的起始地址,Value是卡表索引號的集合)。
由於Region的數量較多,而每個Region都有自己的記憶集,所以G1收集器要花費更大的內存來維持工作,這個數通常是Java堆的10%~20%。
首先,用戶線程改變對象引用關系時,必須保證不打破原本的對象圖結構,導致標記結果出現錯誤。CMS對這個問題采取了增量更新的算法進行解決,而G1選擇了原始快照(SATB)的方法進行解決。
其次,回收過程中會有新對象需要進行內存分配。G1為每個Region設置了兩個名為TAMS的指針,把Region的壹部分空間用於並發回收過程中的新對象分配。新對象的地址必須在這兩個指針之上。這部分空間被收集器視為默認存貨, 不納入回收範圍。
G1收集器的停頓預測模型是以衰減均值作為理論基礎來實現的。在垃圾收集過程中,G1收集器會記錄每個Region的回收時間、記憶集中的臟卡數量等各個步驟的成本,並按照壹定的統計信息和統計算法得出“衰減平均值”。衰減平均值更準確地代表了最近的平均狀態,Region的統計狀態越新就越能決定回收的價值。
根據這些信息,收集器可以決定應當找出哪些Region進入回收集,最終在不超過期望時間的前提下獲得最高收益。
標記壹下GC Roots能直接關聯到的對象。並且修改TAMS的值,讓並發標記階段分配對象有據可依。這個階段需要停頓線程,但耗時很短,並且在Minor GC時同步完成。
從GC Root中開始對堆中對象進行可達性分析,掃描對象圖,找出要回收的對象,這階段耗時長但是可以和用戶程序並發執行。當對象圖掃描完成後,還要重新處理SATB記錄下的在並發時有引用變動的對象。
對用戶線程做壹個短暫的暫停,用於處理並發階段結束後仍遺留下來的最後那些少量的SATB記錄。
負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,制定具體的回收計劃。可以自由地選擇多個Region作為回收集,把其中存活的對象復制到空的Region中,再清理掉整個回收集。由於涉及存活對象的移動,所以必須暫停用戶線程。
總之,G1收集器的設計目標是在延遲可控的前提下獲得盡可能高的吞吐量。
作為兩款關註停頓時間的收集器,G1常被作為CMS收集器的比較對象。在今天,G1已經幾乎完全取代了CMS的地位,但這並不意味著CMS在G1面前不值壹提。
先說明壹個事實:在小內存上CMS的表現可能會優於G1,而大內存上G1毫無疑問會占據優勢。這個堆內存大小的平衡點通常在6~8GB左右。
指定最大停頓時間、分Region的內存布局、按收益確定最終回收集,這些都是G1的新設計給它帶來的相對於CMS的優勢。
CMS集於標記-清除算法進行收集,而G1從整體看集於標記-整理算法進行收集,局部看基於標記-復制算法進行收集。顯然,G1的兩種解讀方法都意味著它不會產生任何內存碎片。這樣的特性有利於程序長久地平穩運行。
我們前文中提到,G1收集器為每壹個Region都提供了卡表作為記憶集,顯然這意味著G1相比CMS需要消耗更大量的內存來完成其本職工作。相比之下CMS的卡表僅有壹份且實現簡單。
CMS使用寫後屏障來更新和維護卡表。G1除了使用寫後屏障,為了實現快照搜索算法,它還得使用寫前屏障來跟蹤並發時的指針變化情況。這也引出了原始快照和增量更新兩種方法的比較:原始快照能夠減少並發標記和重新標記階段的損耗,避免在標記階段停頓時間過長,但它同時也會產生由於跟蹤引用變化帶來的額外負擔。
由於G1寫屏障的復雜操作要比CMS消耗更多的運算資源,CMS的寫屏障實現是直接的同步操作,而G1必須把它實現為類似消息隊列的架構,即把寫前屏障和寫後屏障要做的事都放到隊列裏,然後異步處理。