1. 程式人生 > >Java程式設計師的榮光,聽R大論JDK11的ZGC

Java程式設計師的榮光,聽R大論JDK11的ZGC

前言

ZGC來了 !!! Java程式設計師可以光榮的遠離討厭的GC停頓和調優了。ZGC的成績是,無論你開了多大的堆記憶體(1288G? 2T?),硬是能保證低於10毫秒的JVM停頓。

SPECjbb 2015基準測試,在128G的大堆下,最大停頓時間才 1.68ms (不是平均,不是90%,99%,是Max ! ),遠低於最初的目標-那保守的10ms,也遠勝前代的G1。

大家的第一反應都是這麼顛覆性的東西怎麼來的,G1 通過每次只回收部分Region而不是全堆,改善了大堆下的停頓時間,但在普通大小的堆裡表現並沒驚喜,現在怎麼突然就翻天了,一點心理準備都沒有啊。

如果文章太長不想看下去,你只要記住R大下面這句話就夠了:

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

其實Azul JDK的皇牌 C4 垃圾收集 ,早就同樣以最高十毫秒停頓成為江湖傳說。 曾在Azul的R大, 看著JDK11 ZGC的演算法和結果倍感熟悉,與ZGC的領隊Per Liden大大聊完之後,確認了ZGC跟Azul Pauseless GC,是,等,價,的。(R大御覽本文時 - 其他同學是預覽,R大是御覽,想半天,選定了“等價”這個字眼)

(R大拍的Per大大在JVMLS)

嗯,如果你還有空,下面讓我們來繼續聊聊ZGC的八大特徵。

一、所有階段幾乎都是併發執行的

這裡的併發(Concurrent),說的是應用執行緒與GC執行緒齊頭並進,互不添堵。

說幾乎,就是還有三個非常短暫的STW的階段,所以ZGC並不是Zero Pause GC啦。

R大:“比如開始的Pause Mark Start階段,要做根集合(root set)掃描,包括全域性變數啊、執行緒棧啊啥的裡面的物件指標,但不包括GC堆裡的物件指標,所以這個暫停就不會隨著GC堆的大小而變化(不過會根據執行緒的多少啊、執行緒棧的大小之類的而變化)” -- 因此ZGC可以拍胸脯,無論堆多大停頓都小於10ms。

二、併發執行的保證機制,就是Colored Pointer 和 Load Barrier

原理前面R大一句話已經說完了。Colored Pointer 從64位的指標中,借了幾位出來表示Finalizable、Remapped、Marked1、Marked0。 所以它不支援32位指標也不支援壓縮指標, 且堆的上限是4TB。

有Load barrier在,就會在不同階段,根據指標顏色看看要不要做些特別的事情(Slow Path)。注意下圖裡只有第一種語句需要讀屏障,後面三種都不需要,比如值是原始型別的時候。

R大還提到了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一次。

三、像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能更好的處理大物件的分配。

四、和G1一樣會做Compacting-壓縮

CMS是Mark-Sweep標記過期物件後原地回收,這樣就會造成記憶體碎片,越來越難以找到連續的空間,直到發生Full GC才進行壓縮整理。

ZGC是Mark-Compact ,會將活著的物件都移動到另一個Region,整個回收掉原來的Region。

而G1 是 incremental copying collector,一樣會做壓縮。

下面粗略了幾十倍地過一波回收流程,小階段都被略過了哈:

1. Pause Mark Start -初始停頓標記

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

2. Concurrent Mark -併發標記

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

3. Relocate - 移動物件

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

R大這裡又讚揚了一下C4/ZGC的Quick Release特性:活的物件都移走之後,這個region可以立即釋放掉,並且用來當作下一個要掃描的region的to region。所以理論上要收集整個堆,只需要有一個空region就OK了。

而RedHat的Shenandoah 因為它的forward pointer的設計,則需要有1/2個Heap是空的。

4. Remap - 修正指標

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

五、沒有G1佔記憶體的Remember Set,沒有Write Barrier的開銷

G1 保證“每次GC停頓時間不會過長”的方式,是“每次只清理一部分而不是全部的Region”的增量式清理。

那獨立清理某個Region時 , 就需要有RememberSet來記錄Region之間的物件引用關係, 這樣就能依賴它來輔助計算物件的存活性而不用掃描全堆, RS通常佔了整個Heap的20%或更高。

這裡還需要使用Write Barrier(寫屏障)技術,G1在平時寫引用時,GC移動物件時,都要同步去更新RememberSe,跟蹤跨代跨Region間的引用,特別的重。而CMS裡只有新老生代間的CardTable,要輕很多。

ZGC幾乎沒有停頓,所以劃分Region並不是為了增量回收,每次都會對所有Region進行回收,所以也就不需要這個佔記憶體的RememberSet了,又因為它暫時連分代都還沒實現,所以完全沒有Write Barrier。

六、支援Numa架構

現在多CPU插槽的伺服器都是Numa架構了,比如兩顆CPU插槽(24核),64G記憶體的伺服器,那其中一顆CPU上的12個核,訪問從屬於它的32G本地記憶體,要比訪問另外32G遠端記憶體要快得多。

JDK的 Parallel Scavenger 演算法支援Numa架構,在SPEC JBB 2005 基準測試裡獲得40%的提升。

原理嘛,就是申請堆記憶體時,對每個Numa Node的記憶體都申請一些,當一條執行緒分配物件時,根據當前是哪個CPU在執行的,就在靠近這個CPU的記憶體中分配,這條執行緒繼續往下走,通常會重新訪問這個物件,而且如果執行緒還沒被切換出去,就還是這位CPU同志在訪問,所以就快了。

但可惜CMS,G1不支援Numa,現在ZGC 又重新做了簡單支援,哈哈哈。

七、並行

在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來幹活,非常勤奮。

八、單代

沒分代,應該是ZGC唯一的弱點了。所以R大說ZGC的水平,處於AZul早期的PauselessGC 與 分代的C4演算法之間 - C4在程式碼裡就叫GPGC,Generational Pauseless GC。

分代原本是因為most object die young的假設,而讓新生代和老生代使用不同的GC演算法。但C4已經是全程併發演算法了,為什麼還要分代呢?

R大說:

“因為分代的C4能承受的物件分配速度(Allocation Rate), 大概是原始PGC的10倍。

如果對整個堆做一個完整併發收集週期,持續的時間可能很長比如幾分鐘,而此期間新建立的物件,大致上只能當作活物件來處理,即使它們在這週期裡其實早就死掉可以被收集了。如果有分代演算法,新生物件都在一個專門的區域建立,專門針對這個區域的收集能更頻繁更快,意外留活的物件更也少。

而Per大大因為分代實現起來麻煩,就先實現出比較簡單可用的單代版本。所以ZGC如果遇上非常高的物件分配速率,目前唯一有效的“調優”方式就是增大整個GC堆的大小來讓ZGC有更大的喘息空間。”

小結

ZGC這麼讓Java有面子有期待的事情,不轉不是Java人 !!!

全程各種R大聊天實錄,不轉不是R大粉!!!!

小結2

歇了一年多後的再次更新,因為錯過了公眾號最黃金的時代,麻煩大家重新關注下本號,給深夜碼字的作者一點慰籍。

各位老大寫公眾號推薦集合時,求順帶捎上小號。

轉:江南白衣

公眾號:春天的旁邊

參考資料

1. ZGC wiki:

2. R大的知乎回答:

3. ZGC回收器到底有多變態? by 賀卓凡 ImportSource

本文圖片多有借用,感謝。連結太長不好貼,大家按標題搜尋。

4. A FIRST LOOK INTO ZGC:

5. AZul的《The Pauseless GC Algorithm》論文:

6. AZul開源的C4參考實現,原汁原味的論文實現