天天看點

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

作者:架構思考
在了解 Java 垃圾回收(GC)機制 和 G1 垃圾回收器 後,如何進一步降低 GC 的停頓時間,是目前垃圾回收算法領域研究的最熱點話題之一。歡迎閱讀~

今天就來學習這類旨在減少 GC 停頓的垃圾回收算法 ZGC,ZGC 對 Java 程式員的意義和 G1 是同樣重要的。如果說 CMS 代表的是過去式,而 G1 是一種過渡(盡管這個過渡期會很長),那麼 ZGC 無疑就是 JVM 自動記憶體管理器的未來。

一、什麼是 ZGC

ZGC(The Z Garbage Collector)是 JDK 11 中推出的一款追求極緻低延遲的垃圾收集器。核心是一個并發垃圾回收器,其設計的目标是:

  1. 停頓時間不超過10ms;
  2. 停頓時間不會随着堆的大小,或者活躍對象的大小而增加;
  3. 支援堆範圍為8MB~16TB。

并且官方明确指出 JDK15 中的 ZGC 不再是實驗性質的垃圾收集器,而且建議投入生産了。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

ZGC 的停頓時間到底有多低呢?這裡與 G1 做對比:

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結
「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

二、ZGC的記憶體布局

ZGC 完全抛棄了按代收集理論,它與 G1 一樣将記憶體劃分成各個小的區域的,但與 G1 有所不同的是,ZGC 的各個記憶體區域稱為頁面 (Page),頁面也不是全部大小相等。ZGC按照頁面大小将頁面分為三類:小頁面、中頁面和大頁面。

  • 小頁面:容量固定為 2MB,用于存放小于 256KB 的對象;
  • 中頁面:容量固定為 32MB,用于存放大于等于 256KB 但小于 4MB 的對象
  • 大頁面:容量不固定,可以動态變化,但必須為 2MB 的整數倍,用于存放大于等于 4MB 的對象。
「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

三、ZGC 停頓時間的真相

ZGC 和 G1 有很多相似的地方,它的主體思想也是采用複制活躍對象的方式來回收記憶體。在回收政策上,它也同樣将記憶體分成若幹個區域,回收時也會選擇性地先回收部分區域。

ZGC 與 G1 的差別在于:它可以做到并發轉移(拷貝)對象。并發轉移指的是在對象拷貝的過程中,應用線程和 GC 線程可以同時進行,這是其他 GC 算法目前沒有辦法做到的。

其他的垃圾回收算法,在進行對象轉移時都是需要 Stop The World (STW),而對象轉移往往是垃圾回收過程最耗時的環節,并且耗時會随着堆的增大而增加。ZGC 則不同,在應用線程運作的同時,GC 線程也可以進行對象轉移,這樣就相當于把整個 GC 最耗時的環節放在應用線程背景默默執行,不需要一個長時間的 STW 來等待。這也正是 ZGC 停頓時間很小的主要原因。

如何能在應用線程修改對象引用關系的同時,GC 線程還能正确地轉移對象,或者說 GC 線程将對象轉移的過程中,應用線程是如何通路正在被搬移的對象呢?

四、讀屏障 read barrier

我們知道 CMS 算法和 G1 算法都使用了 write barrier 來保證并發标記的完整性,防止漏标現象。ZGC 的并發标記也不例外。除此之外,ZGC 提升效率的核心關鍵在于并發轉移階段使用了 read barrier。

當應用線程去讀一個對象時,GC 線程剛好正在搬移這個對象。如果 GC 線程沒有搬移完成,那麼應用線程可以去讀這個對象的舊位址;如果這個對象已經搬移完成,那麼可以去讀這個對象的新位址。那麼判斷這個對象是否搬移完成的動作就可以由 read barrier 來完成。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

上圖中,對象 a 和對象 b 都引用了對象 foo,當 foo 正在拷貝的過程中,線程 A 可以通路舊的對象 foo 得到正确的結果,當 foo 拷貝完成之後,線程 B 就可以通過 read barrier 來擷取對象 foo 的新位址,然後直接通路對象 foo 的新位址。

如果這裡隻用 write barrier 是否可行?當 foo 正在拷貝的過程中,線程 A 如果要寫這個對象,那麼隻能在舊的對象 foo 上寫,因為還沒有搬移完成;如果當 foo 拷貝完成之後,線程 B 再去寫對象 foo,是寫到 foo 的新位址,還是舊位址呢?

如果寫到舊位址,那麼對象 foo 就白搬移了,如果寫到新位址,那麼又和線程 A 看到的内容不一樣?是以使用 write barrier 是沒有辦法解決并發轉移過程中線程通路一緻性問題,進而無法保證應用線程的正确性。是以,為了實作并發轉移,ZGC 使用了 read barrier。

同時還可以維護一張映射表(下稱 forwarding table)。在這個映射表中,key 是舊位址,value 是新位址。當對象再次被通路時,通過插入的 read barrier 來判斷對象是否被搬移過。如果 forwarding table 中有這個對象,說明目前通路的對象已經轉移,read barrier 這時就會将對這個對象的引用直接更改為新位址。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

上圖中,當 foo 對象發生轉移之後,對象 a 再通路 foo 時就會觸發 read barrier。read barrier 會查找 forwarding table 來确定對象是否發生了轉移,确定 foo 被轉移到新位址 foo(new)之後,直接将這一次對 foo 的通路更改為 foo(new)。由于整個過程是依托于 read barrier 自動完成的,這個過程也叫“自愈”。

五、染色指針 colored pointer

之前的垃圾收集器都是把GC資訊(标記資訊、GC分代年齡..)存在對象頭的Mark Word裡。

如果某個對象是垃圾對象。ZGC将對象資訊存儲在指針中,這種技術叫做——染色指針(Colored Pointer)。以後不管這個對象在哪兒使用,都知道他是個垃圾對象。

在 64 位系統下,目前 Linux 系統上的位址指針隻用到了 48 位,尋址範圍也就是 256T (2^48 = 256T)。但實際上,目前的應用根本就用不到 256T 記憶體,也沒有哪台伺服器機器上面可以一下插這麼多記憶體條。是以, ZGC 就借用了位址的第 44 ~ 47 位作為标記位,第 0 ~ 44 位共 16T (2^44 = 16T) 的位址空間留做堆使用。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結
  • Marked0 / Marked1:判斷對象是否已标記
  • Remapped:判斷應用是否已指向新的位址
  • Finalizable:判斷對象是否隻能被Finalizer通路

這幾個 bits 在不同的狀态,就代表這個引用的不同顔色。對象标記過程就是打個三色标記,這些标記本質上隻和對象引用有關,和對象本身無關。某個對象隻有它的引用關系才能決定它的存活。

染色指針也會帶來問題,就是修改指針後,作業系統就不認識了。因為染色指針隻是重新定義記憶體中某些指針的其中幾位,OS 又不支援,OS 隻會把整個指針當做一個記憶體位址來對待。為了解決這個問題,ZGC 使用了記憶體多重映射(Multi-Mapping)将多個不同的虛拟記憶體位址映射到同一個實體記憶體位址上,這是一種多對一映射。

六、ZGC 的回收原理

ZGC 的回收過程大緻分為三個主要階段:

  • Mark 階段:标記活躍對象
  • Relocate 階段:活躍對象轉移
  • ReMap 階段:位址視圖統一

Mark — 初始标記

在進行初始标記時,需要進行短暫的 STW。不過在這個階段,ZGC 隻會掃描 root,整個初始标記階段停頓時間很短。停頓時間不會随着堆的增大而增加。

在 GC 開始之前,位址視圖是 Remapped。那麼在 Mark 階段需要做的事情是,将周遊到的對象位址視圖變成 Marked1,1、2、4 對象已經是 marked1,其他對象還是 remapped。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

初始标記

Mark — 并發标記

這裡會繼續周遊整個堆中的存活對象,并将其指針進行染色。5、8 的指針也會被染色為目前視圖下指針,3、6、7 則不變。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

并發标記

至此,所有标記為 Marked1 的對象都認為是活躍對象。而視圖仍是 Remapped 的對象,就認為是垃圾。接下來進入 Relocate 階段,也就是轉移階段

Relocate — 遷移階段

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

準備遷移

根據一定規則去選擇需要遷移的頁,如果頁中有存活的對象,則注冊成存活頁,如果沒有存活對象則直接回收頁。最後再從存活頁中按一定政策選擇需要遷移的頁,并按一定順序填充進遷移集合。填充的操作其實是将頁的資訊封裝成一個個 Forwarding 存到 RlocationSet 中。

Relocate — 初始遷移

這個階段會先切換視圖,從 marked1 切換到 remapped。

之後會掃描與根節點相關的對象,判斷其指針是好是壞。如果是好指針則直接傳回。如果是壞指針,則會先判斷是否在 Forwarding Tables 中,如果沒有就代表不需要遷移,如果有則代表需要進行遷移。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

初始遷移1

先申請塊記憶體,然後将對象 copy 過去,之後把映射關系記錄在 Forwarding Table 中,最後修改 root 指針到新對象上。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

初始遷移2

Relocate — 并發遷移

在這個階段會先周遊 RelocationSet 中所有的 forwarding,從中擷取需要回收的頁資訊,從頁資訊中周遊存活的對象,并對其進行遷移

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

最後則會把之前 relocationSet 中記錄的頁進行回收,這時候紅色箭頭的都是失效的指針都是壞指針,如果使用者通路這些指針會觸發讀屏障進行指針修複。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

回收

Remap 階段

Remap 階段主要是對位址視圖和對象之間的引用關系做修正。因為在 Relocate 階段,GC 線程會将活躍對象快速搬移到新的區域,但是卻不會同時修複對象之間的引用。

這樣就會導緻活躍視圖不統一,需要再對對象的引用關系做一次全面的調整,這個過程也是要周遊所有對象的。不過,因為 Mark 階段也需要周遊所有對象,是以,可以把目前 GC 周期的 Remap 階段和下一個 GC 周期的 Mark 階段複用。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

但是由于 Remap 階段要處理上一輪的 Marked1 視圖指針,又要同時标記下一輪的活躍對象,為了區分,可以再引入一個 Mark 标記,這就是 Marked0 标志。是以 Marked0 和 Marked1 在每一輪 GC 中是交替使用的。

Mark — 第二次 GC 初次标記

第二次 GC 的初次标記階段,由于之前的 marked 标記是 1,現在會切換到 0,是以視圖是從 remapped 切換到 marked0,是以1 2 4 的指針都被染色成 marked0。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

第二次 GC 初次标記

第二此并發标記會将沒有被讀屏障修複的指針進行修複并染色,現在1 2 4 5 8 都被染成 marked0 視圖的指,會将之前儲存的 relocationSet 和 Forwarding Table 都清空。之後的階段就和第一次 GC 一樣。

「後端」Java 程式員必知的 ZGC 垃圾回收器知識總結

第二此并發标記

到這裡,關于 ZGC 的回收流程就說完了,大緻分為三個主要階段:其中 Mark 階段負責标記活躍對象、Relocate 階段負責活躍對象轉移、ReMap 階段負責位址視圖統一。因為 Remap 階段也需要進行全局對象掃描,是以 Remap 和 Mark 階段是重疊進行的。

七、ZGC存在的問題

ZGC最大的問題是浮動垃圾。

ZGC 的停頓時間是在 10ms 以下,但 ZGC 的執行時間還是遠大于這個時間的。假如 ZGC 全過程需要執行 10 分鐘,在這個期間由于對象配置設定速率很高,将建立大量的新對象,這些對象很難進入當次 GC,是以隻能在下次 GC 的時候進行回收,這些隻能等到下次 GC 才能回收的對象就是浮動垃圾。

ZGC沒有分代概念,每次都需要進行全堆掃描,導緻一些“朝生夕死”的對象沒能及時的被回收。

目前唯一的辦法是增大堆的容量,使得程式得到更多的喘息時間,但這個也是一個治标不治本的方案。

八、總結

ZGC 之是以能夠做到這麼低的停頓時間,是因為它的大部分工作都是并發執行的,其中也包括了垃圾回收過程中最耗時的對象轉移階段。

ZGC 能夠做到并發轉移,背後有兩大關鍵技術,分别是 read barrier 和 colored pointer。

read barrier 的作用在于應用線程可以在對象轉移之後,通過 forwarding table 實作"自愈"。而 colored pointer 實作了位址視圖,高效地完成了 read barrier 需要完成的工作,在實作并發轉移的同時,保證吞吐率不出現大幅下降。

ZGC 的回收原理,整個回收過程可以大緻分為 Mark、Relocate、Remap 三個階段,其中 Mark 和 Remap 階段是可以重疊的。

GC 開始時,位址視圖為 Remapped,Mark 階段的主要工作是标記活躍對象,然後将位址視圖向 Marked1 遷移,處于 Marked1 的對象都被認為是活躍對象。

Relocate 階段開始時,位址視圖為 Marked1,該階段主要做對象搬移工作,将位址視圖向 Remapped 遷移。應用線程如果通路一個已經被轉移的對象,就會觸發 read barrier,完成“自愈”,最終通路的是 Remapped 視圖的新對象。

而 Remap 階段是位址視圖的修複階段。在 Remap 階段開始時,位址視圖為 Remapped。Remap 階段的功能是做位址視圖統一,對于仍處于 Marked0 和 Remaped 視圖的活躍對象,将其位址視圖更新為 Marked1。當然也可以是對于仍處于 Marked1 和 Remaped 視圖的活躍對象,将其位址視圖更新為 Marked0。Remap 和 Mark 階段交替進行,交替操作 Marked0 和 Marked1 視圖。

文章來源:https://www.jianshu.com/p/9a6be2e5e246

繼續閱讀