ZGC收集器
歡迎來到我的博客:TWind的博客
我的CSDN::Thanwind-CSDN博客
我的掘金:Thanwinde 的個人主頁
0.前言
ZGC收集器完全可以說是Java收集器的一個跨時代的收集器,他真正意義上實現了停頓時間在10ms以內并且幾乎全時段都是并發的
而且其性能比之前的所有的收集器都更加優越,但初代由于沒有分代機制,導致了其不能承受太高的新建對象的速率
但這些在JDK21中被解決,分代ZGC具有更快的速度以及更低的停頓成為了當之無愧的面向延遲的收集器之王
但值得的一提的是,這并不意為著ZGC是所有情況下的最優解,如你所見,ZGC是一個面向延遲的收集器,它實現了在把延遲降到極致的情況下保持不錯的吞吐量,而且和ShenandoahGC一樣是針對于超大堆(幾百G更高的這種),每個場景都有自己適合的收集器(G1全能的含金量還在上升)
盡管它因為沒有分代設計而導致可能無法適應高內存分配速度的場景,但仍然瑕不掩瑜,而且在JDK21, Generational ZGC 橫空出世,完美解決了這個問題
ZGC收集器
ZGC(Z Garbage Collector),出現于JDK11,主打低延遲,高性能,能控制停頓時間在10ms以內,并實現了基本全程并發
ZGC采用了類似G1的Region的內存分區,參考
但拋棄了分代理論,這帶來了優劣:好處是不用像G1那樣維護龐大且復雜的卡表,壞處是無法享受分區帶來的高效率清理那些“浮動的”對象
為什么拋棄,并不是分代理論不好,而是過于復雜暫時無法實現(在JDK21實現)
這導致了ZGC不能應對高速率創建新對象的場景:如果是分代的話,這些會被劃到新生代針對性處理,而ZGC只會全局掃描一起處理
這就可能導致在清理期間內存又被撐滿,導致不得不全局停頓來清理
但即便如此,ZGC仍然瑕不掩瑜:它低到10ms的延遲,不低的吞吐量非常適合大流量注重延遲的業務,比如:新一代垃圾回收器ZGC的探索與實踐 - 美團技術團隊
現在,讓我們來具體了解一下ZGC
內存布局
ZGC采用了類似于G1的內存布局,將內存分為一個個Region,但是區別在于:
-
沒有新生代,老年代等
-
沒有卡表(沒有跨代)
-
具體大小固定:
-
小型區/頁(
Small
):固定大小為2MB
,用于分配小于256KB
的對象。中型區/頁(
Medium
):固定大小為32MB
,用于分配>=256KB ~ <=4MB
的對象。大型區/頁(
Large
):沒有固定大小,容量可以動態變化,但是大小必須為2MB
的整數倍,專門用于存放>4MB
的巨型對象。但每個Large只能存放一個對象,無論你這個對象多大。而且Large是不會重分配(后面解釋)除了大型區,其他兩個區都可能會容納不止一個對象,且不一定裝滿:剩下的空間會被浪費掉
ZGC拋棄了分代換來了更簡單的內存布局,但是代價就是無法應對高速產生的對象,邏輯分區是這個問題的最優解,可惜ZGC沒有實現,但可喜的是,jdk21 時ZGC補全了這最后一塊拼圖
染色指針
染色指針是什么?可以類比java的對象頭:對象頭存儲了一個對象的基本信息,譬如哈希值,偏向鎖等等,這樣就可以在不實際訪問這個對象的前提下得到這個對象的信息。
在GC中,確定回收哪些對象時要用到三色標記,在ZGC之前都是要用其他的數據結構來維護這個對象的狀態:已遍歷(黑),未遍歷(白),未完全遍歷(灰)這增加了不少了負擔。而染色指針將這個信息直接設法集成到了這個對象的指針當中:省去了查表的操作
具體是:
對于64位的Linux系統來說,一個指針有64位,卻只會用到46位來尋址:46位已然達到了64TB的大小,ZGC就從中提取出了四位用來mark,即使這樣,剩下的42位也有4TB:
- 第一位Finalizable,用來標志這個對象是否是用finaliza,這個功能目前已然廢棄
- 第二位Remapped,意為重映射,可以簡單理解為是用來標志這個對象是否是未活躍:象征著不活躍,會被回收
- 二三位M1和M0,都是用來標記活躍的,區別在于兩個只使用一個,另外一個表示上一次GC的結果,舉個例子,假如對象A第一輪被標記了M0,第二輪時如果沒有被標記,那他還是M0,但這時判斷的是M1,就會把對象A回收,如果都是一個M標記的話,就無法處理這種情況
但這會有一個問題:需要其他的方法來處理指針,讓其能夠正確尋址
一般來說,采用了染色指針會導致內存看上去為原來的三倍
為什么?因為0 1 0 0 + 42位尋址指針,0 0 1 0 + 42位尋址指針 , 0 0 0 1 + 42位尋址指針指向的是同一個地址
其它的內存軟件不會去額外的解析,就會看成三個地址
這意味著JVM必須要對這個地址進行特殊的解析才能正常使用
這樣子的代價除了減少了可用內存,還有不能采用指針壓縮:正常情況下,64位的指針會被壓縮到32位以節省一半的空間
但影響并不大:當內存超過4G本身就會禁用指針壓縮,all in all,染色指針非常有用
具體流程
讓我們再具體的分析:
首先,第一階段:
并發標記
這里和其他的收集器最開始的行為一樣:遍歷所有對象來找到GC root,這里會觸發STW,同時,如果不是第一次GC,這里會順便更新引用
一開始,所有的對象都是Remapped狀態,隨著被標記會變成M0,并且會維護一個的 RSet(回收集),會記錄下要回收的Region,到時候就會把這里面存活的對象移走(如果有)然后回收掉原本的區域
并發預備重分配
這里是并發的,具體來說,這里會掃描整個內存區域以看那些Reign要回收:像G1一樣,會去回收最有價值的Region:比如一個全是待回收的對象的Region顯然是最有價值的
對于這時新建的對象,會默認被標成Remapped
并且對于類卸載以及弱引用也是在這個階段處理的
與G1不同點就出來了:用掃描范圍換取了維護卡表的負擔
并發重分配
這里做的主要就是把并發標記的回收集中的Reigon回收,聽上去簡單,但做著可不見得簡單,具體來說:
會把要留下的對象復制到新的Regio中,同時會維護一個轉發表來記錄對象的老地址和新地址
如果此時有線程來訪問這個老地址,會被JVM的內存屏障捕獲,查看其指針,如果是M0(M1),也就是存活對象,就會被查轉發表轉發到他的新地址,并把這個指針修改成新地址,這被稱之為指針的“自愈”,而且這種比較慢的轉發只會發生一次,因為以后的訪問都會被修正,這個做法比起Shenandoah的轉發指針大幅降低了負載且減少了屏障的使用
如果這時候用戶線程新建了對象,也是Remapped狀態
并發重映射
嚴格來說,這個階段是和并發標記重合的,因為這個階段會去把轉發表里面那些還沒有“自愈”的引用修復
這個行為不是很迫切,因為如果其他線程隨時訪問都能正常訪問到新的地址
目的是在于釋放掉這個轉發表并且避免再訪問老地址會變慢一次的缺陷
因此,ZGC將其合并到了最開始的并發標記階段之中:反正都要遍歷所有的對象,隨便修復了
優勢點
不難看出,ZGC是基于標記-整理的,一定程度上犧牲了清理效率(雖然是被迫的)而帶來了極短的停頓時間
它在中負荷的場景中性能極其優異,但是由于并沒有分代策略,導致其無法應付大量對象快速創建的情景,可能會發生全局停頓導致極大的延時
但它仍然是卓越的,新穎的,而更令人興奮的是,在JDK21, Generational ZGC 橫空出世。