為什麼要學G1
G1(Garbadge First Collector)作為一款JVM最新的垃圾收集器,可以解決CMS中Concurrent Mode Failed問題,儘量縮短處理超大堆的停頓,在G1進行垃圾回收的時候完成內存壓縮,降低內存碎片的生成。G1在堆內存比較大的時候表現出比較高吞吐量和短暫的停頓時間,而且已成為Java 9的默認收集器。未來替代CMS只是時間的問題。
G1的GC原理
Region
G1的內存結構和傳統的內存空間劃分有比較的不同。G1將內存劃分成了多個大小相等的Region(默認是512K),Region邏輯上連續,物理內存地址不連續。同時每個Region被標記成E、S、O、H,分別表示Eden、Survivor、Old、Humongous。其中E、S屬於年輕代,O與H屬於老年代。
示意圖如下:
H表示Humongous。從字面上就可以理解表示大的對象(下面簡稱H對象)。當分配的對象大於等於Region大小的一半的時候就會被認為是巨型對象。H對象默認分配在老年代,可以防止GC的時候大對象的內存拷貝。通過如果發現堆內存容不下H對象的時候,會觸發一次GC操作。
跨代引用
在進行Young GC的時候,Young區的對象可能還存在Old區的引用, 這就是跨代引用的問題。為了解決Young GC的時候,掃描整個老年代,G1引入了Card Table 和Remember Set的概念,基本思想就是用空間換時間。這兩個數據結構是專門用來處理Old區到Young區的引用。Young區到Old區的引用則不需要單獨處理,因為Young區中的對象本身變化比較大,沒必要浪費空間去記錄下來。
- RSet:全稱Remembered Sets, 用來記錄外部指向本Region的所有引用,每個Region維護一個RSet。
- Card: JVM將內存劃分成了固定大小的Card。這裡可以類比物理內存上page的概念。
下圖展示的是RSet與Card的關係。每個Region被分成了多個Card,其中綠色部分的Card表示該Card中有對象引用了其他Card中的對象,這種引用關係用藍色實線表示。RSet其實是一個HashTable,Key是Region的起始地址,Value是Card Table (字節數組),字節數組下標表示Card的空間地址,當該地址空間被引用的時候會被標記為dirty_card。
SATB
SATB的全稱(Snapshot At The Beginning)字面意思是開始GC前存活對象的一個快照。SATB的作用是保證在併發標記階段的正確性。如何理解這句話?
首先要介紹三色標記算法。
- 黑色:根對象,或者該對象與它的子對象都被掃描
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
- 白色:未被掃描對象,掃描完成所有對象之後,最終為白色的為不可達對象,即垃圾對象。
在GC掃描C之前的顏色如下:
在併發標記階段,應用線程改變了這種引用關係:
A.c=C
B.c=null
得到如下結果。
在重新標記階段掃描結果如下:
這種情況下C會被當做垃圾進行回收。Snapshot的存活對象原來是A、B、C,現在變成A、B了,Snapshot的完整遭到破壞了,顯然這個做法是不合理。
G1採用的是pre-write barrier解決這個問題。簡單說就是在併發標記階段,當引用關係發生變化的時候,通過pre-write barrier函數會把這種這種變化記錄並保存在一個隊列裡,在JVM源碼中這個隊列叫satb_mark_queue。在remark階段會掃描這個隊列,通過這種方式,舊的引用所指向的對象就會被標記上,其子孫也會被遞歸標記上,這樣就不會漏標記任何對象,snapshot的完整性也就得到了保證。
SATB的方式記錄活對象,也就是那一時刻對象snapshot, 但是在之後這裡面的對象可能會變成垃圾, 叫做浮動垃圾(floating garbage),這種對象只能等到下一次收集回收掉。在GC過程中新分配的對象都當做是活的,其他不可達的對象就是死的。
如何知道哪些對象是GC開始之後新分配的呢?
在Region中通過top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS來記錄新配的對象。示意圖如下:
每個region記錄著兩個top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。這裡引用R大的解釋。
其中top是該region的當前分配指針,[bottom, top)是當前該region已用(used)的部分,[top, end)是尚未使用的可分配空間(unused)。
(1): [bottom, prevTAMS): 這部分裡的對象存活信息可以通過prevBitmap來得知。
(2): [prevTAMS, nextTAMS): 這部分裡的對象在第n-1輪concurrent marking是隱式存活的。
(3): [nextTAMS, top): 這部分裡的對象在第n輪concurrent marking是隱式存活的。
G1的GC模式
Young GC
Young GC 回收的是所有年輕代的Region。當E區不能再分配新的對象時就會觸發。E區的對象會移動到S區,當S區空間不夠的時候,E區的對象會直接晉升到O區,同時S區的數據移動到新的S區,如果S區的部分對象到達一定年齡,會晉升到O區。
Yung GC過程示意圖如下:
Mixed GC
Mixed GC 翻譯過來叫混合回收。之所以叫混合是因為回收所有的年輕代的Region+部分老年代的Region。
1、為什麼是老年代的部分Region?
2、什麼時候觸發Mixed GC?
這兩個問題其實可以一併回答。回收部分老年代是參數-XX:MaxGCPauseMillis,用來指定一個G1收集過程目標停頓時間,默認值200ms,當然這只是一個期望值。G1的強大之處在於他有一個停頓預測模型(Pause Prediction Model),他會有選擇的挑選部分Region,去儘量滿足停頓時間,關於G1的這個模型是如何建立的,這裡不做深究。
Mixed GC的觸發也是由一些參數控制。比如XX:InitiatingHeapOccupancyPercent表示老年代佔整個堆大小的百分比,默認值是45%,達到該閾值就會觸發一次Mixed GC。
Mixed GC主要可以分為兩個階段:
1、全局併發標記(global concurrent marking)
全局併發標記又可以進一步細分成下面幾個步驟:
- 初始標記(initial mark,STW)。它標記了從GC Root開始直接可達的對象。初始標記階段借用young GC的暫停,因而沒有額外的、單獨的暫停階段。
- 併發標記(Concurrent Marking)。這個階段從GC Root開始對heap中的對象標記,標記線程與應用程序線程並行執行,並且收集各個Region的存活對象信息。過程中還會掃描上文中提到的SATB write barrier所記錄下的引用。
- 最終標記(Remark,STW)。標記那些在併發標記階段發生變化的對象,將被回收。
- 清除垃圾(Cleanup,部分STW)。這個階段如果發現完全沒有活對象的region就會將其整體回收到可分配region列表中。清除空Region。
2、拷貝存活對象(Evacuation)
Evacuation階段是全暫停的。它負責把一部分region裡的活對象拷貝到空region裡去(並行拷貝),然後回收原本的region的空間。Evacuation階段可以自由選擇任意多個region來獨立收集構成收集集合(collection set,簡稱CSet),CSet集合中Region的選定依賴於上文中提到的停頓預測模型,該階段並不evacuate所有有活對象的region,只選擇收益高的少量region來evacuate,這種暫停的開銷就可以(在一定範圍內)可控。
Mixed GC的清理過程示意圖如下:
Full GC
G1的垃圾回收過程是和應用程序併發執行的,當Mixed GC的速度趕不上應用程序申請內存的速度的時候,Mixed G1就會降級到Full GC,使用的是Serial GC。Full GC會導致長時間的STW,應該要儘量避免。
導致G1 Full GC的原因可能有兩個:
- Evacuation的時候沒有足夠的to-space來存放晉升的對象;
- 併發處理過程完成之前空間耗盡
Young GC日誌
通過使用-XX:+PrintGCDetails參數查看的Young GC日誌如下:
① 四個關鍵信息
- 2016-12-12T10:40:18.811-0500:GC發生的時間(通過設置-XX:+PrintGCDateStamps打印)
- 29.959:相對JVM啟動的時間
- G1 Evacuation Pause (young):GC類型,表示這是evacuation停頓,並且是Young GC。
- 0.0305171 sec:本次GC耗時。
② 所有並行任務
- Parallel Time:26.6 。並行任務花費的STW的時間,從收集開始到最後一個GC線程結束。
- GC Workers:4 。並行收集的線程數量。通過 -XX:ParallelGCThreads。當CPU數量小於8時,該值為CPU個數,最大設置成8,對於多於8個的CPU,將默認取CPU個數的5/8。
- GC Worker start:最小|最大時間戳表示第一個線程和最後一個線程的啟動時間。理想情況下希望同時啟動。
- **Ext Root Scanning **:掃描外部根節點的時間。外部節點包括JNI、全局變量、線程棧等。
- **Update RS (Remembered Set or RSet) **:每個線程更新RSet的時間。
- **Scan RS **: 掃描每個CSet中Region的RSet,避免了掃描整個老年代。
- Code Root Scanning:掃描code root耗時。Code Root是JIT編譯後的代碼裡引用了heap中的對象。
- Object Copy:拷貝存活對象到新的Region.
- Termination: 當GC線程完成任務之後嘗試結束到真正結束耗時。因為在結束前他會檢查其他線程是否有未完成的任務,幫助完成之後再結束。
- GC Worker Other:線程花費在其他工作上的時間
- GC Worker Total:每個線程花費的時間總和。
- GC Worker End: 每個線程的結束時間。最小|最大時間戳表示第一個線程和最後一個線程的結束時間。理想情況下希望同時結束。
③ 串行任務
- Code Root Fixup:修復GC期間code root指針改變的耗時
- Code Root Purge:清除code root耗時
- Clear CT:清除card tables 中的dirty card的耗時
④其他事項
其他事項共耗時3.7ms,其他事項包括選擇CSet,處理已用對象,引用入ReferenceQueues,釋放CSet中的region。
⑤各代變化
- Eden: 1097.0M(1097.0M)->0.0B(967.0M):表明了Young GC被觸發,因為Eden區已經滿了(分配了1097M 已經使用了1097.0M),並且Eden區都被清空了(0B),下次垃圾回收Eden區大小降到967M。
- Survivors: 13.0M->139.0M:Young GC之後,Survivor從13M增加到了139M
- Heap: 1694.4M(2048.0M)->736.3M(2048.0M):開始前整個堆佔用了1694.4M,最大可分配2048M,在收集之後,整個堆佔用736M,最大可分配沒有變還是2048M。
⑥ 這次回收耗時
- user=0.08:在垃圾回收時,花費在用戶代碼上的CPU時間。這個時間包含了所有線程運行的CPU時間,所以比real-time大很多
- sys=0.00: 花費在系統內核上的時間。
- real=0.03: 垃圾回收的實際時間。這裡包括了其他進程的時間和等待時間。
①標明標記階段開始
- GC pause (G1 Evacuation Pause) (young) (initial-mark):利用STW停頓期間,跟蹤所有可達對象,該階段和Young GC一起執行。同時該階段也設置兩個指針TAMS來標識已經存在的對象以及在併發標記階段新生成的對象。
② 第一個併發事件 - GC concurrent-root-region-scan-start: 掃描初始化標記階段Survivor區的root Region並標記出來。
③併發標記
- GC concurrent-mark-start:該階段和應用線程一起執行,併發線程數默認是並行線程數的四分之一。可以通過-XX:ConcGCThreads顯示指定。
④ STW階段
- GC remark / Finalize Marking / GC ref-proc / Unloading: 這個階段
⑤ 這也是STW階段
-GC cleanup: 這個階段沒有存活對象的Old Region和Humongous Region將被釋放和清空。為了準備下次GC,在CSets中的Old Regions會根據他們的回收收益的大小排序。為了準備下一次標記,previous bitmaps 和 next bitmaps會被交換。同時並行線程會標記那些inital mark階段生成的對象,以及至少存在一個存活對象的region的bitmap。
⑥這也是一個併發階段
- GC concurrent-cleanup-start:處理第5階段所有空的Region。每個Region中的RSet被清空,當所有的Region都被清理完成,他們會被加入到一個臨時表中,最終會被合併到master free list。
Mixed GC
當併發標記完成後,在Young GC日誌後面緊隨著Mixed GC,下面是Mixed GC日誌。可以看到Mixed GC日誌和前面介紹的Young GC很相似,只有兩個不同點:
1、第一行會表示這是一個Mixed GC
2、收集的集合裡包含了老年代(Old Region),由併發標記階段確定的。
Full GC
Full GC的日誌結果如下。
需要注意的是如果是幾天一次Full GC,則是正常現象,但是每小時頻繁GC就需要調優了。
其他
建議大家開啟-XX:+PrintAdaptiveSizePolicy和-XX:+PrintTenuringDistribution兩個標籤,可以幫助大家更好的分析日誌。
- -XX:+PrintAdaptiveSizePolicy: 顯示收集器工效(Collector ergonomics)
- -XX:+PrintTenuringDistribution: Survivor區的使用和分佈
Young GC開啟-XX:+PrintAdaptiveSizePolicy之後的日誌如下:
① 告訴我們有多少在dirty card隊列裡的cards等待被處理。並且展示了預計處理時間(包括了更新RSet和掃描RSet的時間);
② 有多少Region將被加入到這次GC中
③ 選擇出CSets並且估算這次收集時間
④ 該行並不一定在Young GC日誌中出現,如果花費在GC上的時間比應用線程大到一個閾值的時候,G1可以動態擴大堆大小。如果你設置了最大堆和最小堆的大小相等,該行不會出現
⑤ 當併發標記開始時出現。
Young GC後面是併發回收日誌。
Young GC日誌中還可能存在關於Mixed GC的日誌:
①告訴我們Mixed GC開始,原因是可回收垃圾百分比(22.62%)大於了我們的閾值(5%)。
下面是Mixed GC開啟-XX:+PrintAdaptiveSizePolicy之後執行日誌
① 該階段包括CSet和一部分Young Region的選擇
②描述Mixed GC時,Old Region被加入到CSet中。默認情況下,G1只把10%的Old Region加入到CSet中,通過配置-XX:G1OldCSetRegionThresholdPercent=X可以更改
③提供最終的CSet和停頓預測
④描述Mixed GC狀態細節。在這個案例中,我們仍然有535個Old Region可以被回收,大約305363768字節,佔整個堆大小的14.22%。由於仍然大於閾值,下個階段回收仍然是Mixed GC。
下面是Full GC開啟-XX:+PrintAdaptiveSizePolicy之後執行日誌
① 沒有空的Region用來分配對象,請求擴容堆
② 擴容需要多少空間。到目前為止還沒有真正執行擴容。
③ 不會嘗試擴容。因為沒有可用的Region,所有要執行Full GC。
④ 在最小堆小於最大堆時出現的日誌。G1 在一次Full GC後,嘗試縮小堆到70%。這個百分比可以通過-XX:InitiatingHeapOccupancyPercent(IHOP)調節,這個參數設置使用整個對的x%時,系統開始進行並行GC。注意是整個堆的百分比。
⑤ 堆正在被縮小,已經縮小了多少容量。
-XX:+PrintTenuringDistribution: 可以查看每次回收期間,Survivor區的分佈信息。可以幫助我們查看對象年齡的變化。
上圖主要從三個層面展示Survivor區:
-desired survivor size: 期望的Survivor大小。該值等於Survivor大小乘以TargetSurvivorRatio (默認50%)。
- target threshold:目標閾值。表示一個對象的年齡,這個值可以通過每個年齡的所有對象大小相加直到大於desired survivor size計算出來。
- age distribution: 年齡分佈。包括了每個年齡所有對象的大小以及增量Survivor區大小
閱讀更多 大數據每日嗶嗶 的文章