G1(Garbage First)收集器 (標記-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基于“標記-整理”算法實現,也就是說不會產生內存碎片。此外,G1收集器不同于之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代),而其他收集器回收的范圍僅限于新生代或老年代。
?
?
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中,-XX:+UseG1GC用于開啟 G1 垃圾收集器,-Xmx32g用于設置堆內存的最大內存為 32G,-XX:MaxGCPauseMillis=200用于設置 GC 的最大暫停時間為 200ms。如果我們需要調優,在內存大小一定的情況下,我們只需要修改最大暫停時間即可。
?
關鍵字
LAB
由于分區的思想,每個線程均可以"認領"某個分區用于線程本地的內存分配,而不需要顧及分區是否連續。因此,每個應用線程和GC線程都會獨立的使用分區,進而減少同步時間,提升GC效率,這個分區稱為本地分配緩沖區(Lab)。
?
TLAB
應用線程可以獨占一個本地緩沖區(TLAB)來創建的對象,而大部分都會落入Eden區域(巨型對象或分配失敗除外),因此TLAB的分區屬于Eden空間;
?
GCLAB
每次垃圾收集時,每個GC線程同樣可以獨占一個本地緩沖區(GCLAB)用來轉移對象,每次回收會將對象復制到Suvivor空間或老年代空間;
?
PLAB
對于從Eden/Survivor空間晉升(Promotion)到Survivor/老年代空間的對象,同樣有GC獨占的本地緩沖區進行操作,該部分稱為晉升本地緩沖區(PLAB)。
?
G1堆內存結構
?
?
堆內存中一個區域 (Region) 的大小,可以通過 -XX:G1HeapRegionSize 參數指定,大小區間最小 1M 、最大 32M ,總之是 2 的冪次方。
?
默認是將堆內存按照 2048 份均分。
?
每個 Region 被標記了 E、S、O 和 H,這些區域在邏輯上被映射為 Eden,Survivor 和老年代。
?
存活的對象從一個區域轉移(即復制或移動)到另一個區域。區域被設計為并行收集垃圾,可能會暫停所有應用線程。
?
此外,還有第四種類型,被稱為巨型區域(Humongous Region)。
?
G1中的region的大小,由參數G1HeapRegionSize定義,如果沒有定義,就由xms/2048計算region大小,如果小于1就取1,如果大于32就取32,如果是其他值,就取2,4,8,16相近的數值,總之是2的n次方。
?
G1中的region的數量不一定是2048,如果內存小于2G,每個region最小為1M,那么數量就小于2048,比如內存超過64g,每個region最大為32M,那么數量也就超過2048,例如,128g,那么region數量就為4096個。
?
巨形對象Humongous Region
存儲超過 50% 標準 region 大小的對象稱為巨型對象(Humongous Object)。當線程為巨型分配空間時,不能簡單在TLAB進行分配,因為巨型對象的移動成本很高,而且有可能一個分區不能容納巨型對象。因此,巨型對象會直接在老年代分配,所占用的連續空間稱為巨型分區(Humongous Region)。G1內部做了一個優化,一旦發現沒有引用指向巨型對象,則可直接在年輕代收集周期中被回收。
?
巨型對象會獨占一個、或多個連續分區,其中第一個分區被標記為開始巨型(StartsHumongous),相鄰連續分區被標記為連續巨型(ContinuesHumongous)。由于無法享受Lab帶來的優化,并且確定一片連續的內存空間需要掃描整堆,因此確定巨型對象開始位置的成本非常高,如果可以,應用程序應避免生成巨型對象。
?
如果一個 H 區裝不下一個巨型對象,那么 G1 會尋找連續的 H 分區來存儲。為了能找到連續的 H 區,有時候不得不啟動 Full GC 。
?
Remember Set:
? 在串行和并行收集器中,GC時是通過整堆掃描來確定對象是否處于可達路徑中。然而G1為了避免STW式的整堆掃描,為每個分區各自分配了一個 RSet(Remembered Set),它內部類似于一個反向指針,記錄了其它 Region 對當前 Region 的引用情況,這樣就帶來一個極大的好處:回收某個Region時,不需要執行全堆掃描,只需掃描它的 RSet 就可以找到外部引用,來確定引用本分區內的對象是否存活,進而確定本分區內的對象存活情況,而這些引用就是 initial mark 的根之一。
?
? 事實上,并非所有的引用都需要記錄在RSet中,如果引用源是本分區的對象,那么就不需要記錄在 RSet 中;同時 G1 每次 GC 時,所有的新生代都會被掃描,因此引用源是年輕代的對象,也不需要在RSet中記錄;所以最終只需要記錄老年代到新生代之間的引用即可。
?
RSet 的寫屏障
? 寫屏障是指,每次 Reference 引用類型在執行寫操作時,都會產生 Write Barrier 寫屏障暫時中斷操作并額外執行一些動作。
?
? 對寫屏障來說,過濾掉不必要的寫操作是十分有必要的,因為寫柵欄的指令開銷是十分昂貴的,這樣既能加快賦值器的速度,也能減輕回收器的負擔。G1 收集器的寫屏障是跟 RSet 相輔相成的,產生寫屏障時會檢查要寫入的引用指向的對象是否和該 Reference 類型數據在不同的 Region,如果不同,才通過 CardTable 把相關引用信息記錄到引用指向對象的所在 Region 對應的 RSet 中,通過過濾就能使 RSet 大大減少。
?
(1)寫前柵欄:即將執行一段賦值語句時,等式左側對象將修改引用到另一個對象,那么等式左側對象原先引用的對象所在分區將因此喪失一個引用,那么JVM就需要在賦值語句生效之前,記錄喪失引用的對象。但JVM并不會立即維護RSet,而是通過批量處理,在將來RSet更新
?
(2)寫后柵欄:當執行一段賦值語句后,等式右側對象獲取了左側對象的引用,那么等式右側對象所在分區的RSet也應該得到更新。同樣為了降低開銷,寫后柵欄發生后,RSet也不會立即更新,同樣只是記錄此次更新日志,在將來批量處理
? G1垃圾回收器進行垃圾回收時,在GC根節點枚舉范圍加入RSet,就可以保證不進行全局掃描,也不會有遺漏。另外JVM使用的其余的分代的垃圾回收器也都有寫屏障,舉例來說,每次將一個老年代對象的引用修改為指向年輕代對象,都會被寫屏障捕獲并記錄下來,因此在年輕代回收的時候,就可以避免掃描整個老年代來查找根。
?
G1的垃圾回收器的寫屏障使用一種兩級的log buffer結構:
?
global set of filled buffer:所有線程共享的一個全局的,存放填滿了的log buffer的集合
thread log buffer:每個線程自己的log buffer。所有的線程都會把寫屏障的記錄先放進去自己的log buffer中,裝滿了之后,就會把log buffer放到 global set of filled buffer中,而后再申請一個log buffer;