JVM - ZGC初探
文章目錄
- Pre
- ZGC概述
- ZGC的目標
- 參數
- 不分代(暫時)
- ZGC的內存布局
- NUMA-aware 非統一內存訪問自動感知
- ZGC的回收階段
- 顏色指針
- 讀屏障
- ZGC觸發時機
- 存在的問題[浮動垃圾]
- 參考資料
Pre
JVM - G1初探
ZGC概述
ZGC是一款JDK 11中新加入的具有實驗性質的低延遲垃圾收集器,ZGC源自于是Azul System公司開發的C4(Concurrent Continuously Compacting Collector) 收集器。
目前很少有公司使用,可適當了解,擴展知識面。
ZGC的目標
- 支持TB量級的堆。 這可是上T的內存哇,誰家這么壕~
- 最大GC停頓時間不超10ms。目前一般線上環境運行良好的JAVA應用Minor GC停頓時間在10ms左右,Major GC一般都需要100ms以上(G1可以調節停頓時間,但是如果調的過低的話,反而會適得其反),之所以能做到這一點是因為它的停頓時間主要跟Root掃描有關,而Root數量和堆大小是沒有任何關系的。
- 奠定未來GC特性的基礎。
- 最糟糕的情況下吞吐量會降低15%。這都不是事,停頓時間足夠優秀。至于吞吐量,通過擴容分分鐘解決。
另外,Oracle官方提到了它最大的優點是:它的停頓時間不會隨著堆的增大而增長!也就是說,幾十G堆的停頓時間是10ms以下,幾百G甚至上T堆的停頓時間也是10ms以下。
參數
不分代(暫時)
單代,即ZGC「沒有分代」。
以前的垃圾回收器之所以分代,是因為源于“「大部分對象朝生夕死」”的假設,事實上大部分系統的對象分配行為也確實符合這個假設。
ZGC的內存布局
ZGC收集器是一款基于Region內存布局的, 暫時不設分代的, 使用了讀屏障、 顏色指針等技術來實現可并發的標記-整理算法的, 以低延遲為首要目標的一款垃圾收集器。
ZGC的Region可以具有大、 中、 小三類容量:
- 小型Region(Small Region) : 容量固定為2MB, 用于放置小于256KB的小對象。
- 中型Region(Medium Region) : 容量固定為32MB, 用于放置大于等于256KB但小于4MB的對象。
- 大型Region(Large Region) : 容量不固定, 可以動態變化, 但必須為2MB的整數倍, 用于放置4MB或以上的大對象。
NUMA-aware 非統一內存訪問自動感知
UMA即Uniform Memory Access Architecture
NUMA是Non Uniform Memory Access Architecture
UMA表示內存只有一塊,所有CPU都去訪問這一塊內存,那么就會存在競爭問題(爭奪內存總線訪問權),有競爭就會有鎖,有鎖效率就會受到影響,而且CPU核心數越多,競爭就越激烈。
NUMA的話每個CPU對應有一塊內存,且這塊內存在主板上離這個CPU是最近的,每個CPU優先訪問這塊內存,那效率自然就提高了
ZGC的回收階段
- 并發標記(Concurrent Mark):
與G1一樣,并發標記是遍歷對象圖做可達性分析的階段,它的初始標記(Mark Start)和最終標記(Mark End)也會出現短暫的停頓,與G1不同的是, ZGC的標記是在指針上而不是在對象上進行的, 標記階段會更新顏色指針(見下圖)Marked 0、 Marked 1標志位
- 并發預備重分配(Concurrent Prepare for Relocate)
根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。ZGC每次回收都會掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護成本。
- 并發重分配(Concurrent Relocate)
核心階段
把重分配集中的存活對象復制到新的Region上,并為重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關系。ZGC收集器能僅從引用上就明確得知一個對象是否處于重分配集之中,如果用戶線程此時并發訪問了位于重分配集中的對象,這次訪問將會被預置的內存屏障(讀屏障(見下面詳解))所截獲,然后立即根據Region上的轉發表記錄將訪問轉發到新復制的對象上,并同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行為稱為指針的“自愈”(Self-Healing)能力。
ZGC的顏色指針因為“自愈”(Self-Healing)能力,所以只有第一次訪問舊對象會變慢, 一旦重分配集中某個Region的存活對象都復制完畢后, 這個Region就可以立即釋放用于新對象的分配,但是轉發表還得留著不能釋放掉, 因為可能還有訪問在使用這個轉發表。
- 并發重映射(Concurrent Remap)
修正整個堆中指向重分配集中舊對象的所有引用,但是ZGC中對象引用存在“自愈”功能,所以這個重映射操作并不是很迫切。ZGC很巧妙地把并發重映射階段要做的工作,合并到了下一次垃圾收集循環中的并發標記階段里去完成,反正它們都是要遍歷所有對象的,這樣合并就節省了一次遍歷對象圖的開銷。一旦所有指針都被修正之后, 原來記錄新舊對象關系的轉發表就可以釋放掉了。
顏色指針
ZGC的核心設計之一。以前的垃圾回收器的GC信息都保存在對象頭中,而ZGC的GC信息保存在指針中。
每個對象有一個64位指針,這64位被分為:
- 18位:預留給以后使用;
- 1位:Finalizable標識,此位與并發引用處理有關,它表示這個對象只能通過finalizer才能訪問;
- 1位:Remapped標識,設置此位的值后,對象未指向relocation set中(relocation set表示需要GC的Region集合);
- 1位:Marked1標識;
- 1位:Marked0標識,和上面的Marked1都是標記對象用于輔助GC;
- 42位:對象的地址(所以它可以支持2^42=4T內存)
為什么有2個mark標記?
每一個GC周期開始時,會交換使用的標記位,使上次GC周期中修正的已標記狀態失效,所有引用都變成未標記。
GC周期1:使用mark0, 則周期結束所有引用mark標記都會成為01。
GC周期2:使用mark1, 則期待的mark標記10,所有引用都能被重新標記。
通過對配置ZGC后對象指針分析我們可知,對象指針必須是64位,那么ZGC就無法支持32位操作系統,同樣的也就無法支持壓縮指針了(CompressedOops,壓縮指針也是32位)。
顏色指針的三大優勢
讀屏障
之前的GC都是采用Write Barrier,這次ZGC采用了完全不同的方案讀屏障,這個是ZGC一個非常重要的特性。
在標記和移動對象的階段,每次「從堆里對象的引用類型中讀取一個指針」的時候,都需要加上一個Load Barriers。
ZGC觸發時機
ZGC目前有4中機制觸發GC:
- 定時觸發,默認為不使用,可通過ZCollectionInterval參數配置。
- 預熱觸發,最多三次,在堆內存達到10%、20%、30%時觸發,主要時統計GC時間,為其他GC機制使用。
- 分配速率,基于正態分布統計,計算內存99.9%可能的最大分配速率,以及此速率下內存將要耗盡的時間點,在耗盡之前觸發GC(耗盡時間 - 一次GC最大持續時間 - 一次GC檢測周期時間)。
- 主動觸發,(默認開啟,可通過ZProactive參數配置) 距上次GC堆內存增長10%,或超過5分鐘時,對比距上次GC的間隔時間跟(49 * 一次GC的最大持續時間),超過則觸發。
存在的問題[浮動垃圾]
ZGC最大的問題是浮動垃圾。ZGC的停頓時間是在10ms以下,但是ZGC的執行時間還是遠遠大于這個時間的。
假如ZGC全過程需要執行10分鐘,在這個期間由于對象分配速率很高,將創建大量的新對象,這些對象很難進入當次GC,所以只能在下次GC的時候進行回收,這些只能等到下次GC才能回收的對象就是浮動垃圾。
ZGC沒有分代概念,每次都需要進行全堆掃描,導致一些“朝生夕死”的對象沒能及時的被回收。
解決方案:
目前唯一的辦法是增大堆的容量,使得程序得到更多的喘息時間,但是這個也是一個治標不治本的方案。如果需要從根本上解決這個問題,還是需要引入分代收集,讓新生對象都在一個專門的區域中創建,然后專門針對這個區域進行更頻繁、更快的收集。
參考資料
https://wiki.openjdk.java.net/display/zgc/Main
http://cr.openjdk.java.net/~pliden/slides/ZGC-Jfokus-2018.pdf
總結
以上是生活随笔為你收集整理的JVM - ZGC初探的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM - G1初探
- 下一篇: JVM - 解读GC中的 Safe P