本文已收錄至Github,推薦閱讀 ?? Java隨想錄
微信公眾號:Java隨想錄
CSDN: 碼農BookSea
目錄人生下來不是為了拖著鎖鏈,而是為了展開雙翼,——雨果
- 基于Region的堆記憶體布局
- 可預測的停頓時間模型
- 跨Region參考物件
- 物件參考關系改變
- 運作程序
- CMS VS G1
- 相關引數
Garbage First(簡稱G1)收集器是垃圾收集器技術發展歷史上的里程碑式的成果,它開創了收集器面向區域收集的設計思路和基于Region的記憶體布局形式,設計者們設計G1的時候希望G1能夠建立起“停頓時間模型”,停頓時間模型的意思是能夠支持指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過N毫秒這樣的目標,下文會有所講述,
基于Region的堆記憶體布局
首先,介紹下G1基于Region的堆記憶體布局,這是能夠能夠建立起“停頓時間模型”的關鍵,
G1邏輯上分代,但是物理上不分代,
G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間,收集器能夠對扮演不同角色的Region采用不同的策略去處理,
G1可以通過引數控制新生代記憶體大小的引數:-XX:G1NewSizePercent(默認等于5),-XX:G1MaxNewSizePercent(默認等于60),也就是說新生代大小默認占整個堆記憶體的 5% ~ 60%,
這樣就不存在界限,無論是新創建的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果,
使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region的大小可以通過引數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪,
可以簡單算一下,G1能管理的最大記憶體大約 32MB * 2048 = 64G左右,
Region中還有一類特殊的Humongous區域,專門用來存盤大物件,G1認為只要大小超過了一個Region容量一半的物件(即超過1.5個region)即可判定為大物件,而對于那些超過了整個Region容量的超級大物件,將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待,
可預測的停頓時間模型
G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的記憶體空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集,
G1收集器會去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然后在后臺維護一個優先級串列,每次根據用戶設定允許的收集停頓時間(使用引數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來,
這種使用Region劃分記憶體空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取盡可能高的收集效率,
所以說G1實作可預測的停頓時間模型的關鍵就是Region布局和優先級佇列,看起來好像G1的實作也不復雜,但是其實有許多細節是需要考慮的,
跨Region參考物件
G1將Java堆分成多個獨立Region后,Region里面存在的跨Region參考物件如何解決?
解決方案的思路我們已經知道,使用記憶集,
但是麻煩的是,G1的堆記憶體是以Region為基本回收單位的,所以它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指標,并標記這些指標分別在哪些卡頁的范圍之內,
G1的記憶集在存盤結構的本質上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,里面存盤的元素是卡表的索引號,
由于Region數量較多,每個Region都維護有自己的記憶集,光是存盤記憶集這塊就要占用相當一部分記憶體,G1比其他圾收集器有著更高的記憶體占用負擔,根據經驗,G1至少要耗費大約相當于Java堆容量10%至20%的額外記憶體來維持收集器作業,
物件參考關系改變
如何處理用戶執行緒改變物件參考關系?
之前說過,G1收集器則是通過原始快照(SATB)演算法來實作的,
垃圾收集對用戶執行緒的影響還體現在回收程序中新創建物件的記憶體分配上,程式要繼續運行就肯定會持續有新物件被創建,G1為每一個Region設計了兩個名為TAMS(Top at Mark Start)的指標,把Region中的一部分空間劃分出來用于并發回收程序中的新物件分配,并發回收時新分配的物件地址都必須要在這兩個指標位置以上,G1收集器默認在這個地址以上的物件是被隱式標記過的,即默認它們是存活的,不納入回收范圍,與CMS中的“Concurrent Mode Failure”失敗會導致Full GC類似,如果記憶體回收的速度趕不上記憶體分配的速度,G1收集器也要被迫凍結用戶執行緒執行,導致Full GC而產生長時間“Stop The World”,
用戶通過-XX:MaxGCPauseMillis引數指定的停頓時間只意味著垃圾收集發生之前的期望值,在垃圾收集程序中,G1收集器會記錄每個Region的回收耗時、每個Region記憶集里的臟卡數量等各個可測量的步驟花費的成本,并分析得出平均值、標準偏差、置信度等統計資訊,然后通過這些資訊預測現在開始回收的話,由哪些Region組成回收集才可以在不超過期望停頓時間的約束下獲得最高的收益,
運作程序
- 初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的物件,并且修改TAMS指標的值,讓下一階段用戶執行緒并發運行時,能正確地在可用的Region中分配新物件,這個階段需要停頓執行緒,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓,
- 并發標記(Concurrent Marking):從GC Root開始對堆中物件進行可達性分析,遞回掃描整個堆里的物件圖,找出要回收的物件,這階段耗時較長,但可與用戶程式并發執行,當物件圖掃描完成以后,還要重新處理SATB記錄下的在并發時有參考變動的物件,
- 最終標記(Final Marking):對用戶執行緒做另一個短暫的暫停,用于處理并發階段結束后仍遺留下來的最后那少量的SATB記錄,
- 篩選回收(Live Data Counting and Evacuation):負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活物件復制到空的Region中,再清理掉整個舊Region的全部空間,這里的操作涉及存活物件的移動,是必須暫停用戶執行緒,由多條收集器執行緒并行完成的,
- 從上述階段的描述可以看出,G1收集器除了并發標記外,其余階段也是要完全暫停用戶執行緒的,
- G1從整體來看是基于“標記-整理”演算法實作的收集器,但從區域(兩個Region之間)上看又是基于“標記-復制”演算法實作,
G1默認的停頓目標為兩百毫秒,但如果我們把停頓時間調得非常低,譬如設定為二十毫秒,很可能出現的結果就是由于停頓目標時間太短,導致每次選出來的回收集只占堆記憶體很小的一部分,收集器收集的速度逐漸跟不上分配器分配的速度,導致垃圾慢慢堆積,應用運行時間一長,最終占滿堆引發Full GC反而降低性能,所以通常把期望停頓時間設定為一兩百毫秒或者兩三百毫秒會是比較合理的,
CMS VS G1
相比CMS,G1的優點有很多,較為明顯的優點就是G1不會產生垃圾碎片,不過,G1相對于CMS仍然不是占全方位、壓倒性優勢的,至少G1無論是為了垃圾收集產生的記憶體占用(Footprint)還是程式運行時的額外執行負載(Overload)都要比CMS要高,
就記憶體占用來說,雖然G1和CMS都使用卡表來處理跨代指標,但G1的每個Region都必須有一份卡表,這導致G1的記憶集可能會占整個堆容量的20%乃至更多的記憶體空間;相比起來CMS的卡表就相當簡單,全域只有一份,
在執行負載的角度上,譬如它們都使用到寫屏障,CMS用寫后屏障來更新維護卡表;而G1除了使用寫后屏障來進行同樣的卡表維護操作外,為了實作原始快照搜索(SATB)演算法,還需要使用寫前屏障來跟蹤并發時的指標變化情況,相比起增量更新演算法,原始快照搜索能夠減少并發標記和重新標記階段的消耗,避免CMS那樣在最終標記階段停頓時間過長的缺點,但是在用戶程式運行程序中確實會產生由跟蹤參考變化帶來的額外負擔,由于G1對寫屏障的復雜操作要比CMS消耗更多的運算資源,所以CMS的寫屏障實作是直接的同步操作,而G1就不得不將其實作為類似于訊息佇列的結構,把寫前屏障和寫后屏障中要做的事情都放到佇列里,然后再異步處理,目前在小記憶體應用上CMS的表現大概率仍然要會優于G1,而在大記憶體應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間,
相關引數
| 引數 | 描述 |
|---|---|
| -XX:+UseG1GC | 手動指定使用G1收集器執行記憶體回收任務(JDK9后不用設定,默認就是G1) |
| -XX:G1HeapRegionSize | 設定每個Region的大小,值是2的冪,范圍是1MB到32MB之間,目標是根據最小的Java堆大小劃分出約2048個區域,默認是堆記憶體的1/2000 |
| -XX:MaxGCPauseMillis | 設定期望達到的最大GC停頓時間指標 |
| -XX:InitiatingHeapOccupancyPercent | 簡稱為IHOP,設定觸發并發GC周期的Java堆占用率閾值,超過此值,就觸發GC,默認值是45% |
| -XX:+G1UseAdaptiveIHOP | 自動調整IHOP的指,JDK9之后可用 |
| -XX:GCTimeRatio | 這個引數為0~100之間的整數(G1默認是9),值為 n 則系統將花費不超過 1/(1+n) 的時間用于垃圾收集,因此G1默認最多 10% 的時間用于垃圾收集 |
如果本篇博客有任何錯誤和建議,歡迎給我留言指正,文章持續更新,可以關注公眾號第一時間閱讀,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/542666.html
標籤:Java
上一篇:JVM詳記
