文章首發于公衆号《程式員果果》
位址 :
https://mp.weixin.qq.com/s/gfdml-SfvhFdMXlAu-a61w
一、簡介
Java 11包含一個全新的垃圾收集器--ZGC,它由Oracle開發,承諾在數TB的堆上具有非常低的暫停時間。 在本文中,我們将介紹開發新GC的動機,技術概述以及由ZGC開啟的一些可能性。
那麼為什麼需要新GC呢?畢竟Java 10已經有四種釋出多年的垃圾收集器,并且幾乎都是無限可調的。 換個角度看,G1是2006年時引入Hotspot VM的。當時最大的AWS執行個體有1 vCPU和1.7GB記憶體,而今天AWS很樂意租給你一個x1e.32xlarge執行個體,該類型執行個體有128個vCPU和3,904GB記憶體。 ZGC的設計目标是:支援TB級記憶體容量,暫停時間低(<10ms),對整個程式吞吐量的影響小于15%。 将來還可以擴充實作機制,以支援不少令人興奮的功能,例如多層堆(即熱對象置于DRAM和冷對象置于NVMe閃存),或壓縮堆。
二、GC術語
為了了解ZGC如何比對現有收集器,以及如何實作新GC,我們需要先了解一些術語。最基本的垃圾收集涉及識别不再使用的記憶體并使其可重用。現代收集器在幾個階段進行這一過程,對于這些階段我們往往有如下描述:
- 并行:在JVM運作時,同時存在應用程式線程和垃圾收集器線程。 并行階段是由多個gc線程執行,即gc工作在它們之間配置設定。 不涉及GC線程是否需要暫停應用程式線程。
- 串行:串行階段僅在單個gc線程上執行。與之前一樣,它也沒有說明GC線程是否需要暫停應用程式線程。
- STW:STW階段,應用程式線程被暫停,以便gc執行其工作。 當應用程式因為GC暫停時,這通常是由于Stop The World階段。
- 并發:如果一個階段是并發的,那麼GC線程可以和應用程式線程同時進行。 并發階段很複雜,因為它們需要在階段完成之前處理可能使工作無效。
- 增量:如果一個階段是增量的,那麼它可以運作一段時間之後由于某些條件提前終止,例如需要執行更高優先級的gc階段,同時仍然完成生産性工作。 增量階段與需要完全完成的階段形成鮮明對比。
三、工作原理
現在我們了解了不同gc階段的屬性,讓我們繼續探讨ZGC的工作原理。 為了實作其目标,ZGC給Hotspot Garbage Collectors增加了兩種新技術:着色指針和讀屏障。
着色指針
着色指針是一種将資訊存儲在指針(或使用Java術語引用)中的技術。因為在64位平台上(ZGC僅支援64位平台),指針可以處理更多的記憶體,是以可以使用一些位來存儲狀态。 ZGC将限制最大支援4Tb堆(42-bits),那麼會剩下22位可用,它目前使用了4位: finalizable, remap, mark0和mark1。 我們稍後解釋它們的用途。
着色指針的一個問題是,當您需要取消着色時,它需要額外的工作(因為需要屏蔽資訊位)。 像SPARC這樣的平台有内置硬體支援指針屏蔽是以不是問題,而對于x86平台來說,ZGC團隊使用了簡潔的多重映射技巧。
多重映射
要了解多重映射的工作原理,我們需要簡要解釋虛拟記憶體和實體記憶體之間的差別。 實體記憶體是系統可用的實際記憶體,通常是安裝的DRAM晶片的容量。 虛拟記憶體是抽象的,這意味着應用程式對(通常是隔離的)實體記憶體有自己的視圖。 作業系統負責維護虛拟記憶體和實體記憶體範圍之間的映射,它通過使用頁表和處理器的記憶體管理單元(MMU)和轉換查找緩沖器(TLB)來實作這一點,後者轉換應用程式請求的位址。
多重映射涉及将不同範圍的虛拟記憶體映射到同一實體記憶體。 由于設計中隻有一個remap,mark0和mark1在任何時間點都可以為1,是以可以使用三個映射來完成此操作。 ZGC源代碼中有一個很好的圖表可以說明這一點。
讀屏障
讀屏障是每當應用程式線程從堆加載引用時運作的代碼片段(即通路對象上的非原生字段non-primitive field):
void printName( Person person ) {
String name = person.name; // 這裡觸發讀屏障
// 因為需要從heap讀取引用
//
System.out.println(name); // 這裡沒有直接觸發讀屏障
}
在上面的代碼中,String name = person.name 通路了堆上的person引用,然後将引用加載到本地的name變量。此時觸發讀屏障。 Systemt.out那行不會直接觸發讀屏障,因為沒有來自堆的引用加載(name是局部變量,是以沒有從堆加載引用)。 但是System和out,或者println内部可能會觸發其他讀屏障。
這與其他GC使用的寫屏障形成對比,例如G1。讀屏障的工作是檢查引用的狀态,并在将引用(或者甚至是不同的引用)傳回給應用程式之前執行一些工作。 在ZGC中,它通過測試加載的引用來執行此任務,以檢視是否設定了某些位。 如果通過了測試,則不執行任何其他工作,如果失敗,則在将引用傳回給應用程式之前執行某些特定于階段的任務。
标記
現在我們了解了這兩種新技術是什麼,讓我們來看看ZG的GC循環。
GC循環的第一部分是标記。标記包括查找和标記運作中的應用程式可以通路的所有堆對象,換句話說,查找不是垃圾的對象。
ZGC的标記分為三個階段。 第一階段是STW,其中GC roots被标記為活對象。 GC roots類似于局部變量,通過它可以通路堆上其他對象。 如果一個對象不能通過周遊從roots開始的對象圖來通路,那麼應用程式也就無法通路它,則該對象被認為是垃圾。從roots通路的對象集合稱為Live集。GC roots标記步驟非常短,因為roots的總數通常比較小。
該階段完成後,應用程式恢複執行,ZGC開始下一階段,該階段同時周遊對象圖并标記所有可通路的對象。 在此階段期間,讀屏障針使用掩碼測試所有已加載的引用,該掩碼确定它們是否已标記或尚未标記,如果尚未标記引用,則将其添加到隊列以進行标記。
在周遊完成之後,有一個最終的,時間很短的的Stop The World階段,這個階段處理一些邊緣情況(我們現在将它忽略),該階段完成之後标記階段就完成了。
重定位
GC循環的下一個主要部分是重定位。重定位涉及移動活動對象以釋放部分堆記憶體。 為什麼要移動對象而不是填補空隙? 有些GC實際是這樣做的,但是它導緻了一個不幸的後果,即配置設定記憶體變得更加昂貴,因為當需要配置設定記憶體時,記憶體配置設定器需要找到可以放置對象的空閑空間。 相比之下,如果可以釋放大塊記憶體,那麼配置設定記憶體就很簡單,隻需要将指針遞增新對象所需的記憶體大小即可。
ZGC将堆分成許多頁面,在此階段開始時,它同時選擇一組需要重定位活動對象的頁面。選擇重定位集後,會出現一個Stop The World暫停,其中ZGC重定位該集合中root對象,并将他們的引用映射到新位置。與之前的Stop The World步驟一樣,此處涉及的暫停時間僅取決于root的數量以及重定位集的大小與對象的總活動集的比率,這通常相當小。是以不像很多收集器那樣,暫停時間随堆增加而增加。
移動root後,下一階段是并發重定位。 在此階段,GC線程周遊重定位集并重新定位其包含的頁中所有對象。 如果應用程式線程試圖在GC重新定位對象之前加載它們,那麼應用程式線程也可以重定位該對象,這可以通過讀屏障(在從堆加載引用時觸發)實作,如流程圖如下所示:
這可確定應用程式看到的所有引用都已更新,并且應用程式不可能同時對重定位的對象進行操作。
GC線程最終将對重定位集中的所有對象重定位,然而可能仍有引用指向這些對象的舊位置。 GC可以周遊對象圖并重新映射這些引用到新位置,但是這一步代價很高昂。 是以這一步與下一個标記階段合并在一起。在下一個GC周期的标記階段周遊對象對象圖的時候,如果發現未重映射的引用,則将其重新映射,然後标記為活動狀态。
概括
試圖單獨了解複雜垃圾收集器(如ZGC)的性能特征是很困難的,但從前面的部分可以清楚地看出,我們所碰到的幾乎所有暫停都隻依賴于GC roots集合大小,而不是實時堆大小。标記階段中處理标記終止的最後一次暫停是唯一的例外,但是它是增量的,如果超過gc時間預算,那麼GC将恢複到并發标記,直到再次嘗試。
三、性能
那ZGC到底表現如何?
Stefan Karlsson和Per Liden在今年早些時候的Jfokus演講中給出了一些數字。 ZGC的SPECjbb 2015吞吐量與Parallel GC(優化吞吐量)大緻相當,但平均暫停時間為1ms,最長為4ms。 與之相比G1和Parallel有很多次超過200ms的GC停頓。
然而,垃圾收集器是複雜的軟體,從基準測試結果可能無法推測出真實世界的性能。我們期待自己測試ZGC,以了解它的性能如何因工作負載而異。
本文參考:
https://mp.weixin.qq.com/s/nAjPKSj6rqB_eaqWtoJsgw