ZGC的成績(jī)是锅睛,無論你開了多大的堆內(nèi)存(1288G? 2T历谍?),硬是能保證低于10毫秒的JVM停頓辣垒。
SPECjbb 2015基準(zhǔn)測(cè)試望侈,在128G的大堆下,最大停頓時(shí)間才 1.68ms (不是平均勋桶,不是90%脱衙,99%,是Max ! )例驹,遠(yuǎn)低于最初的目標(biāo)-那保守的10ms捐韩,也遠(yuǎn)勝前代的G1。
大家的第一反應(yīng)都是這么顛覆性的東西怎么來的鹃锈,G1 通過每次只回收部分Region而不是全堆荤胁,改善了大堆下的停頓時(shí)間,但在普通大小的堆里表現(xiàn)并沒驚喜∈赫現(xiàn)在怎么突然就翻天了仅政,一點(diǎn)心理準(zhǔn)備都沒有啊。
如果文章太長(zhǎng)不想看下去盆驹,你只要記住R大下面這句話就夠了:
與標(biāo)記對(duì)象的傳統(tǒng)算法相比圆丹,ZGC在指針上做標(biāo)記,在訪問指針時(shí)加入Load Barrier(讀屏障)躯喇,比如當(dāng)對(duì)象正被GC移動(dòng)辫封,指針上的顏色就會(huì)不對(duì),這個(gè)屏障就會(huì)先把指針更新為有效地址再返回,也就是倦微,永遠(yuǎn)只有單個(gè)對(duì)象讀取時(shí)有概率被減速妻味,而不存在為了保持應(yīng)用與GC一致而粗暴整體的Stop The World。
其實(shí)Azul JDK的皇牌 C4 垃圾收集 璃诀,早就同樣以最高十毫秒停頓成為江湖傳說弧可。 曾在Azul的R大, 看著JDK11 ZGC的算法和結(jié)果倍感熟悉劣欢,與ZGC的領(lǐng)隊(duì)Per Liden大大聊完之后棕诵,確認(rèn)了ZGC跟Azul Pauseless GC,是凿将,等校套,價(jià),的
[圖片上傳失敗...(image-68a273-1648567376486)]
下面讓我們來繼續(xù)聊聊ZGC的八大特征牧抵。
一笛匙、所有階段幾乎都是并發(fā)執(zhí)行的
這里的并發(fā)(Concurrent),說的是應(yīng)用線程與GC線程齊頭并進(jìn)犀变,互不添堵妹孙。
說幾乎,就是還有三個(gè)非常短暫的STW的階段获枝,所以ZGC并不是Zero Pause GC啦蠢正。
R大:“比如開始的Pause Mark Start階段,要做根集合(root set)掃描省店,包括全局變量啊嚣崭、線程棧啊啥的里面的對(duì)象指針,但不包括GC堆里的對(duì)象指針懦傍,所以這個(gè)暫停就不會(huì)隨著GC堆的大小而變化(不過會(huì)根據(jù)線程的多少啊雹舀、線程棧的大小之類的而變化)” -- 因此ZGC可以拍胸脯,無論堆多大停頓都小于10ms粗俱。
二说榆、并發(fā)執(zhí)行的保證機(jī)制,就是Colored Pointer 和 Load Barrier
原理前面R大一句話已經(jīng)說完了寸认。Colored Pointer 從64位的指針中娱俺,借了幾位出來表示Finalizable、Remapped废麻、Marked1荠卷、Marked0。 所以它不支持32位指針也不支持壓縮指針烛愧, 且堆的上限是4TB油宜。
有Load barrier在掂碱,就會(huì)在不同階段,根據(jù)指針顏色看看要不要做些特別的事情(Slow Path)慎冤。注意下圖里只有第一種語句需要讀屏障疼燥,后面三種都不需要,比如值是原始類型的時(shí)候蚁堤。
R大還提到了ZGC的Load Value Barrier醉者,與Red Hat的Shenandoah收集器的不同,后者選擇了70年代的比較基礎(chǔ)的Brooks Pointer 披诗,而前者在也是很老的Baker barrier上加入了self healing的特性撬即,比如下面的代碼:
Object a = obj.x;
Object b = obj.x;
兩行代碼都插入了讀屏障,但ZGC在第一個(gè)讀屏障之后呈队,不但a的值是新的剥槐,self healing下obj.x的值自身也會(huì)修正,第二個(gè)讀屏障時(shí)就直接進(jìn)入FastPath宪摧,沒有消耗了粒竖; 而Shenandoah 則不會(huì)修正obj.x的值,第二個(gè)讀屏障又要SlowPath一次几于。
三蕊苗、像G1一樣劃分Region,但更加靈活
ZGC將堆劃分為Region作為清理沿彭,移動(dòng)岁歉,以及并行GC線程工作分配的單位。
不過G1一開始就把堆劃分成固定大小的Region膝蜈,而ZGC 可以有2MB,32MB熔掺,N× 2MB 三種Size Groups饱搏,動(dòng)態(tài)地創(chuàng)建和銷毀Region,動(dòng)態(tài)地決定Region的大小置逻。
256k以下的對(duì)象分配在Small Page推沸, 4M以下對(duì)象在Medium Page,以上在Large Page券坞。
所以ZGC能更好的處理大對(duì)象的分配鬓催。
四、和G1一樣會(huì)做Compacting-壓縮
CMS是Mark-Sweep標(biāo)記過期對(duì)象后原地回收恨锚,這樣就會(huì)造成內(nèi)存碎片宇驾,越來越難以找到連續(xù)的空間,直到發(fā)生Full GC才進(jìn)行壓縮整理猴伶。
ZGC是Mark-Compact 课舍,會(huì)將活著的對(duì)象都移動(dòng)到另一個(gè)Region塌西,整個(gè)回收掉原來的Region。
而G1 是 incremental copying collector筝尾,一樣會(huì)做壓縮捡需。
下面粗略了幾十倍地過一波回收流程,小階段都被略過了哈:
1. Pause Mark Start -初始停頓標(biāo)記
停頓JVM地標(biāo)記Root對(duì)象筹淫,1站辉,2,4三個(gè)被標(biāo)為live损姜。
2. Concurrent Mark -并發(fā)標(biāo)記
并發(fā)地遞歸標(biāo)記其他對(duì)象饰剥,5和8也被標(biāo)記為live。
3. Relocate - 移動(dòng)對(duì)象
對(duì)比發(fā)現(xiàn)3薛匪、6捐川、7是過期對(duì)象,也就是中間的兩個(gè)灰色region需要被壓縮清理逸尖,所以陸續(xù)將4古沥、5、8 對(duì)象移動(dòng)到最右邊的新Region娇跟。移動(dòng)過程中岩齿,有個(gè)forward table紀(jì)錄這種轉(zhuǎn)向。
R大這里又贊揚(yáng)了一下C4/ZGC的Quick Release特性:活的對(duì)象都移走之后苞俘,這個(gè)region可以立即釋放掉盹沈,并且用來當(dāng)作下一個(gè)要掃描的region的to region。所以理論上要收集整個(gè)堆吃谣,只需要有一個(gè)空region就OK了乞封。
而RedHat的Shenandoah 因?yàn)樗膄orward pointer的設(shè)計(jì),則需要有1/2個(gè)Heap是空的岗憋。
4. Remap - 修正指針
最后將指針都妥帖地更新指向新地址肃晚。這里R大還提到一個(gè)亮點(diǎn): “上一個(gè)階段的Remap,和下一個(gè)階段的Mark是混搭在一起完成的仔戈,這樣非常高效关串,省卻了重復(fù)遍歷對(duì)象圖的開銷〖嗯牵”
五晋修、沒有G1占內(nèi)存的Remember Set,沒有Write Barrier的開銷
G1 保證“每次GC停頓時(shí)間不會(huì)過長(zhǎng)”的方式凰盔,是“每次只清理一部分而不是全部的Region”的增量式清理墓卦。
那獨(dú)立清理某個(gè)Region時(shí) , 就需要有RememberSet來記錄Region之間的對(duì)象引用關(guān)系, 這樣就能依賴它來輔助計(jì)算對(duì)象的存活性而不用掃描全堆户敬, RS通常占了整個(gè)Heap的20%或更高趴拧。
這里還需要使用Write Barrier(寫屏障)技術(shù)溅漾,G1在平時(shí)寫引用時(shí),GC移動(dòng)對(duì)象時(shí)著榴,都要同步去更新RememberSe添履,跟蹤跨代跨Region間的引用,特別的重脑又。而CMS里只有新老生代間的CardTable暮胧,要輕很多。
ZGC幾乎沒有停頓问麸,所以劃分Region并不是為了增量回收往衷,每次都會(huì)對(duì)所有Region進(jìn)行回收,所以也就不需要這個(gè)占內(nèi)存的RememberSet了严卖,又因?yàn)樗鼤簳r(shí)連分代都還沒實(shí)現(xiàn)席舍,所以完全沒有Write Barrier。
六哮笆、支持Numa架構(gòu)
現(xiàn)在多CPU插槽的服務(wù)器都是Numa架構(gòu)了来颤,比如兩顆CPU插槽(24核),64G內(nèi)存的服務(wù)器稠肘,那其中一顆CPU上的12個(gè)核福铅,訪問從屬于它的32G本地內(nèi)存,要比訪問另外32G遠(yuǎn)端內(nèi)存要快得多项阴。
JDK的 Parallel Scavenger 算法支持Numa架構(gòu)滑黔,在SPEC JBB 2005 基準(zhǔn)測(cè)試?yán)铽@得40%的提升。
原理嘛环揽,就是申請(qǐng)堆內(nèi)存時(shí)略荡,對(duì)每個(gè)Numa Node的內(nèi)存都申請(qǐng)一些,當(dāng)一條線程分配對(duì)象時(shí)歉胶,根據(jù)當(dāng)前是哪個(gè)CPU在運(yùn)行的汛兜,就在靠近這個(gè)CPU的內(nèi)存中分配,這條線程繼續(xù)往下走跨扮,通常會(huì)重新訪問這個(gè)對(duì)象,而且如果線程還沒被切換出去验毡,就還是這位CPU同志在訪問衡创,所以就快了。
但可惜CMS晶通,G1不支持Numa璃氢,現(xiàn)在ZGC 又重新做了簡(jiǎn)單支持,哈哈哈狮辽。
R大補(bǔ)充一也,G1也打算支持了Numa了: http://openjdk.java.net/jeps/157
七巢寡、并行
在ZGC 官網(wǎng)上有介紹,前面基準(zhǔn)測(cè)試中的32核服務(wù)器椰苟,128G堆的場(chǎng)景下抑月,它的配置是:
20條ParallelGCThreads,在那三個(gè)極短的STW階段并行的干活 - mark roots舆蝴, weak root processing(StringTable, JNI Weak Handles,etc)和 relocate roots 谦絮;
4條ConcGCThreads,在其他階段與應(yīng)用并發(fā)地干活 - Mark洁仗,Process Reference层皱,Relocate。 僅僅四條赠潦,高風(fēng)亮節(jié)地盡量不與應(yīng)用爭(zhēng)搶CPU 叫胖。
ConcCGCThreads開始時(shí)各自忙著自己平均分配下來的Region,如果有線程先忙完了她奥,會(huì)嘗試“偷”其他線程還沒做的Region來干活瓮增,非常勤奮。
八方淤、單代
沒分代钉赁,應(yīng)該是ZGC唯一的弱點(diǎn)了。所以R大說ZGC的水平携茂,處于AZul早期的PauselessGC 與 分代的C4算法之間 - C4在代碼里就叫GPGC你踩,Generational Pauseless GC。
分代原本是因?yàn)閙ost object die young的假設(shè)讳苦,而讓新生代和老生代使用不同的GC算法带膜。但C4已經(jīng)是全程并發(fā)算法了,為什么還要分代呢鸳谜?
R大說:
“因?yàn)榉执腃4能承受的對(duì)象分配速度(Allocation Rate)膝藕, 大概是原始PGC的10倍。
如果對(duì)整個(gè)堆做一個(gè)完整并發(fā)收集周期咐扭,持續(xù)的時(shí)間可能很長(zhǎng)比如幾分鐘芭挽,而此期間新創(chuàng)建的對(duì)象,大致上只能當(dāng)作活對(duì)象來處理蝗肪,即使它們?cè)谶@周期里其實(shí)早就死掉可以被收集了袜爪。如果有分代算法,新生對(duì)象都在一個(gè)專門的區(qū)域創(chuàng)建薛闪,專門針對(duì)這個(gè)區(qū)域的收集能更頻繁更快辛馆,意外留活的對(duì)象更也少。
而Per大大因?yàn)榉执鷮?shí)現(xiàn)起來麻煩豁延,就先實(shí)現(xiàn)出比較簡(jiǎn)單可用的單代版本昙篙。所以ZGC如果遇上非常高的對(duì)象分配速率腊状,目前唯一有效的“調(diào)優(yōu)”方式就是增大整個(gè)GC堆的大小來讓ZGC有更大的喘息空間√桑”