Garbage First(簡稱 G1)收集器是垃圾收集器技術發展史上里程碑式的成果:它開創了「面向局部收集」的設計思路和「基於 Region」的內存佈局形式。算法
G1 收集器是一款主要面向服務端應用的垃圾收集器,其定位是「CMS 收集器的替代者和繼承人」。它的發展簡史以下:bash
JDK 7 Update 40 時,Oracle 認爲它達到了足夠成熟的商用程度;併發
JDK 8 Update 40 時,G1 收集器提供了併發的類卸載支持,被 Oracle 稱爲「全功能的垃圾收集器(Fully-Featured Garbage Collector)」。異步
JDK 9 中,G1 取代 Parallel Scavenge + Parallel Old 組合,成爲服務端模式下的默認垃圾收集器。而 CMS 收集器則不推薦(Deprecate)使用了。佈局
雖然 G1 收集器也遵循分代收集理論,但其堆內存的佈局與其餘收集器有很是明顯的差別:spa
G1 再也不堅持固定大小和固定數量的分代區域劃分,而是把連續的 Java 堆劃分爲多個大小相等的獨立區域(Region),每一個 Region 均可以根據須要,扮演新生代的 Eden 空間、Survivor 空間,或者老年代空間。線程
Region 中還有一類特殊的 Humongous 區域,專門來存儲大對象(大小超過一個 Region 容量的一半)。設計
而對於超過整個 Region 的超大對象,將會被存在 N 個連續的 Humongous Region 中(G1 的大多數行爲都把 Humongous Region 做爲老年代的一部分看待)。3d
G1 收集器的堆內存劃分如圖所示:指針
停頓時間模型(Pause Prediction Model):指定在一個長度爲 M 毫秒的時間片斷內,消耗在垃圾收集上的時間大機率不超過 N 毫秒。
G1 收集器之因此能創建可預測的停頓時間模型,是由於它將 Region 做爲單次回收的最小單元(每次收集到的內存空間都是 Region 大小的整數倍),這樣能夠有計劃地避免整個 Java 堆進行全區域垃圾收集。
更具體的處理思路:讓 G1 收集器去跟蹤各個 Region 中的垃圾堆積的「價值」大小,而後在後臺維護一個優先級列表,每次根據用戶設定的收集停頓時間,優先處理回收價值收益最大的那些 Region(這就是「Garbage First」名字的由來)。
「價值」的衡量指標是:每次回收所得到的空間大小以及回收所需時間的經驗值。
G1 收集器以前的其餘全部收集器(包括 CMS 收集器),垃圾收集的目標範圍要麼是整個新生代(Minor GC),或者整個老年代(Major GC),抑或整個 Java 堆(Full GC)。
而 G1 跳出了這個樊籠:它能夠面向堆內存中任何部分來組成回收集(Collection Set,通常稱 CSet)進行回收。衡量標準再也不是它屬於哪一個分代,而是哪塊內存中存放的垃圾數量最多、回收收益最大。這就是 G1 收集器的 Mixed GC 模式。
G1 收集器的運行示意圖以下:
它的運做過程大體可分爲如下四個步驟:
主要工做
僅標記 GC Roots 能直接關聯到的對象。
修改 TAMS 指針的值,使得下一階段用戶線程併發運行時,能正確地在可用的 Region 中分配新對象。
特色
須要停頓用戶線程,但耗時很短,且是借用 Minor GC 時同步完成的。
TAMS:Top at Mark Start,Region 中的指針,用於併發標記時爲對象分配內存空間。
主要工做
從 GC Root 開始對堆中對象進行可達性分析,遞歸掃描整個堆裏的對象圖,找出要回收的對象。
特色
耗時較長
可與用戶程序併發執行
此外,掃描完成後,還須要從新處理 STAB 記錄下的在併發時有引用變更的對象。
STAB: Snapshot At The Begining,原始快照,參考前文「JVM筆記-HotSpot的算法細節實現」的 6.3 小節。
主要工做
處理併發標記結束後仍遺留下來的最後少許的 STAB 記錄。
特色
須要暫停用戶線程(時間較短)。
主要工做
更新 Region 統計數據,對各個 Region 的回收價值和成本進行排序。
根據用戶所指望的停頓時間來制定回收計劃,能夠自由選擇任意多個 Region 構成回收集,而後把決定回收的那一部分 Region 的存活對象複製到空的 Region 中,再清理掉整個舊 Region 的所有空間。
特色
因爲涉及存活對象的移動,須要暫停用戶線程。
G1 將堆內存劃分爲多個 Region,那些跨 Region 引用對象如何處理呢?
解決思路就是使用前文「JVM筆記-HotSpot的算法細節實現」第 4 小節的「記憶集」來避免全堆做爲 GC Roots 掃描。
可是,G1 的記憶集更復雜,由於:
卡表是雙向的(「我指向誰」、「誰指向我」),比原先的卡表更復雜;
Region 數量比傳統收集器的分代數量多出不少,每一個 Region 都要維護本身的記憶集,所以 G1 收集器比其餘的傳統垃圾收集器有更高的內存佔用負擔。
根據經驗,G1 至少要耗費大約 Java 堆容量大小的 10%~20% 的額外內存空間來維持收集器工做。
併發標記階段如何保證收集線程與用戶線程互不干擾地運行呢?
解決思路是前文第 6 小節分析的:CMS 收集器使用增量更新算法,而 G1 收集器則是經過原始快照(STAB)算法實現的。
TAMS
此外,因爲併發標記時用戶線程仍在繼續執行,確定會持續建立新對象。
G1 爲每一個 Region 設計了兩個名爲 TAMS(Top at Mark Start)的指針,把 Region 中的一部分空間劃分出來用於併發回收過程當中的新對象分配(默認都是存活的,不歸入回收範圍)。
須要注意的是:若是內存回收速度趕不上內存分配的速度,G1 收集器也要被迫凍結用戶線程執行,致使 Full GC 而產生長時間「Stop The World」。
如何創建可靠的停頓預測模型(知足用戶設定的指望停頓時間)?
G1 收集器的停頓模型是以衰減均值(Decaying Average)爲理論基礎來實現的:垃圾收集過程當中,G1 收集器會根據每一個 Region 的回收耗時、記憶集中的髒卡數量等,分析得出平均值、標準誤差等。
「衰減平均值」比普通的平均值更能準確地表明「最近的」平均狀態,經過這些信息預測如今開始回收的話,由哪些 Region 組成回收集才能在不超指望停頓時間的約束下得到最高收益。
G1 收集器常常會被拿來與 CMS 收集器進行比較。
且不論 G1 的一些創新設計:能夠指定最大停頓時間、分 Region 的內存佈局、按收益動態肯定回收集等,這裏只對比一些其餘較爲通用的地方。
CMS:「標記-清除」算法
G1
總體:「標記-整理」算法
局部:「標記-複製」算法
G1 的這兩種算法使其在運做期間不會產生內存空間碎片,垃圾收集完成後能提供規整的可用內存。並且這樣有利於程序長時間運行(大對象分配內存時不容易因沒法找到連續內存空間而提早觸發下一次收集)。
CMS 和 G1 都使用卡表來處理跨代指針,但 G1 的卡表實現更復雜,且 Region 較多(本文 5.1 小節)。
相比而言,CMS 的卡表相對簡單,只有一份,只需處理老年代到新生代的引用。
與 CMS 相比,G1 的內存佔用會更大。
因爲兩者細節實現不一樣致使用戶程序執行時負載會有不一樣。以寫屏障爲例:
CMS:使用寫後屏障維護卡表;
G1:除了寫後屏障維護卡表,爲了實現原始快照(STAB)算法,還需使用寫前屏障跟蹤併發時的指針變化。
G1 的寫屏障比 CMS 要消耗更多的運算資源。所以,CMS 寫屏障是同步操做,而 G1 則是採用相似消息隊列的異步操做。
總體而言:
小內存應用上,CMS 大機率會優於 G1;
大內存應用上,G1 則極可能更勝一籌。
這個臨界點大概是在 6~8G 之間(經驗值)。
一些相關的虛擬機參數以下:
# 使用 G1 收集器-XX:+UseG1GC
# 設置 Region 大小(範圍 1~32M,且爲 2 的 N 次冪)
-XX:G1HeapRegionSize
# 最大收集停頓時間(默認 200 毫秒)
-XX:MaxGCPauseMillis複製代碼