1. 程式人生 > 程式設計 >JDK11的ZGC的回收過程-學習筆記

JDK11的ZGC的回收過程-學習筆記

前兩天看到一篇不錯的文章,連線在這裡:聽R大論JDK11的ZGC,文章寫的很不錯,能夠說明白ZGC的整個過程。

前言

ZGC來了 !!! Java程式設計師可以光榮的遠離討厭的GC停頓和調優了。ZGC的成績是,無論你開了多大的堆記憶體(128G?2T?),硬是能保證低於10毫秒的JVM停頓。遠低於最初的G1 avg:156.806ms。ZGC的目標保守的10ms,也遠勝前代的G1 。
下面放張圖片鎮樓:

與標記物件的傳統演演算法相比,ZGC在指標上做標記,在訪問指標時加入Load Barrier(讀屏障),比如當物件正被GC移動,指標上的顏色就會不對,這個屏障就會先把指標更新為有效地址再返回,也就是,永遠只有單個物件讀取時有概率被減速,而不存在為了保持應用與GC一致而粗暴整體的Stop The World。

下面來說下ZGC的八大特徵:

1.所有階段幾乎都是併發執行的

這裡的併發(Concurrent),說的是應用執行緒與GC執行緒齊頭並進,互不添堵。 所有階段幾乎都是併發執行的中的幾乎,就是說ZGC還有三個非常短暫的STW的階段,所以ZGC並不是Zero Pause GC啦。
比如開始的Pause Mark Start階段,要做根集合(root set)掃描,包括全域性變數、執行緒棧啥的裡面的物件指標,但不包括GC堆裡的物件指標,所以這個暫停就不會隨著GC堆的大小而變化(不過會根據執行緒的多少啊、執行緒棧的大小之類的而變化), 因此ZGC無論堆多大停頓都小於10ms。

2.併發執行的保證機制,就是Colored Pointer(著色指標) 和 Load Barrier(讀屏障)

Colored Pointer從64位的指標中,借用了幾位出來表示Finalizable、Remapped、Marked1、Marked0。 所以它不支援32位指標也不支援壓縮指標, 且堆的上限是4TB。首先要明確的是ZGC只使用與64位作業系統。不適合32位作業系統,32位的最多4G記憶體。

有Load barrier(讀屏障)在,就會在不同階段,根據指標顏色看看要不要做些特別的事情(Slow Path)。注意下圖裡只有第一種語句需要讀屏障,後面三種都不需要,比如值是原始型別的時候。
ZGC的Load Value Barrier,與Red Hat的Shenandoah收集器的不同,後者選擇了70年代的比較基礎的Brooks Pointer,而前者在也是很老的Baker barrier上加入了self healing的特性,比如下面的程式碼:

Object a = obj.x; 
Object b = obj.x;
複製程式碼

兩行程式碼都插入了讀屏障,但ZGC在第一個讀屏障之後,不但a的值是新的,self healing下obj.x的值自身也會修正,第二個讀屏障時就直接進入FastPath,沒有消耗了; 而Shenandoah則不會修正obj.x的值,第二個讀屏障又要SlowPath一次。

3.像G1一樣劃分Region,但更加靈活

ZGC將堆劃分為Region作為清理,移動,以及並行GC執行緒工作分配的單位。 不過G1一開始就把堆劃分成固定大小的Region,而ZGC 可以有2MB,32MB,N× 2MB 三種Size Groups,動態地建立和銷燬Region,動態地決定Region的大小。
256k以下的物件分配在Small Page, 4M以下物件在Medium Page,以上在Large Page。
所以ZGC能更好的處理大物件的分配。

4.和G1一樣會做Compacting-壓縮

CMS是Mark-Sweep標記過期物件後原地回收,這樣就會造成記憶體碎片,越來越難以找到連續的空間,直到發生Full GC才進行壓縮整理。
ZGC是Mark-Compact ,會將活著的物件都移動到另一個Region,整個回收掉原來的Region。
而G1 是 incremental copying collector,一樣會做壓縮。
下面粗略的瞭解一波回收流程,其他詳細小階段都被略過了哈:

4.1 Pause Mark Start -初始停頓標記

停頓JVM地標記Root物件,1,2,4三個被標為live。

4.2 Concurrent Mark -併發標記

併發地遞迴標記其他物件,5和8也被標記為live活物件。

4.3 Relocate -移動物件

對比發現3、6、7是過期物件,也就是中間的兩個灰色region需要被壓縮清理,所以陸續將4、5、8物件移動到最右邊的新Region。移動過程中,有個forward table紀錄這種轉向。

活的物件都移走之後,這個region可以立即釋放掉,並且用來當作下一個要掃描的region的to region。所以理論上要收集整個堆,只需要有一個空region就OK了。

4.4 Remap -修正指標

最後將指標都妥帖地更新指向新地址。上一個階段的Remap,和下一個階段的Mark是混搭在一起完成的,這樣非常高效,省卻了重複遍歷物件圖的開銷。

5.沒有G1佔記憶體的Remember Set,沒有Write Barrier的開銷

G1 保證“每次GC停頓時間不會過長”的方式,是“每次只清理一部分而不是全部的Region”的增量式清理。
G1在獨立清理某個Region時,就需要有RememberSet來記錄Region之間的物件引用關係, 這樣就能依賴它來輔助計算物件的存活性而不用掃描全堆, Remembered Set通常佔了整個Heap的20%或更高。 這裡G1還使用Write Barrier(寫屏障)技術,G1在平時寫引用時,GC移動物件時,都要同步去更新RememberSet,跟蹤跨代跨Region間的引用,特別的重。而在CMS裡只有新老生代間的CardTable,要輕很多。
ZGC幾乎沒有停頓(10ms),所以劃分Region並不是為了增量回收,每次都會對所有Region進行回收,所以也就不需要這個佔記憶體的RememberSet了,又因為它暫時連分代都還沒實現,所以完全沒有Write Barrier。

6.支援Numa架構

現在多CPU插槽的伺服器都是Numa架構(這個要google)了,比如兩顆CPU插槽(24核),64G記憶體的伺服器,那其中一顆CPU上的12個核,訪問從屬於它的32G本地記憶體,要比訪問另外32G遠端記憶體要快得多。
JDK的 Parallel Scavenger 演演算法支援Numa架構,在SPEC JBB 2005 基準測試裡獲得40%的提升。
至於原理,就是申請堆記憶體時,對每個Numa Node的記憶體都申請一些,當一條執行緒分配物件時,根據當前是哪個CPU在執行的,就在靠近這個CPU的記憶體中分配,這條執行緒繼續往下走,通常會重新訪問這個物件,而且如果執行緒還沒被切換出去,就還是這位CPU同志在訪問,所以就快了。
但可惜CMS,G1不支援Numa,現在ZGC又重新做了簡單支援。

7.並行

ZGC官網上有介紹,前面基準測試中的32核伺服器,128G堆的場景下,它的配置是:
20條ParallelGCThreads,在那三個極短的STW階段並行的幹活 - mark roots, weak root processing(StringTable,JNI Weak Handles,etc)和 relocate roots ;
4條ConcGCThreads,在其他階段與應用併發地幹活 - Mark,Process Reference,Relocate。 僅僅四條,高風亮節地儘量不與應用爭搶CPU 。
ConcCGCThreads開始時各自忙著自己平均分配下來的Region,如果有執行緒先忙完了,會嘗試“偷”其他執行緒還沒做的Region來幹活,非常勤奮。

8.單代

沒分代,應該是ZGC唯一的弱點了(我為啥一定要用分代了?)。
分代原本是因為most object die young的假設,而讓新生代和老生代使用不同的GC演演算法。
如果對整個堆做一個完整併發收集週期,持續的時間可能很長比如幾分鐘,而此期間新建立的物件,大致上只能當作活物件來處理,即使它們在這週期裡其實早就死掉可以被收集了。如果有分代演演算法,新生物件都在一個專門的區域建立,專門針對這個區域的收集能更頻繁更快,意外留活的物件更也少。