一文讀懂Java 11的ZGC爲何如此高效

導讀:GC是大部分現代語言內置的特性,Java 11 新加入的ZGC號稱可以達到10ms 以下的 GC 停頓,本文作者對這一新功能進行了深入解析。同時還對還對這一新功能帶來的其他可能性做了展望。ZGC是否可以達到該性能目標,請看高可用架構志願者翻譯的文章。

Java 11的新功能已經完全凍結,其中有些功能絕對非常令人興奮,本文着重介紹ZGC。

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階段,同時仍然完成生產性工作。 增量階段與需要完全完成的階段形成鮮明對比。

權衡

值得指出的是,所有這些屬性都需要權衡利弊。 例如,並行階段將利用多個gc線程來執行工作,但這樣做會導致線程協調的開銷。 同樣,併發階段不會暫停應用程序線程,但可能涉及更多的開銷和複雜性,才能同時處理使其工作無效的應用程序線程。

ZGC

現在我們瞭解了不同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):

voidprintName( 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階段,這個階段處理一些邊緣情況(我們現在將它忽略),該階段完成之後標記階段就完成了。這裏給大家推薦一個Java技術交流羣;828545509

重定位

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,以瞭解它的性能如何因工作負載而異。

未來的可能性

着色指針和讀屏障提供了一些有趣的可能。

多層堆和壓縮

隨着閃存和非易失性存儲器變得越來越普遍,一種可能是JVM中允許多層堆,可以讓很少使用的對象存儲在較慢的存儲層上。

該功能可以通過擴展指針元數據來實現,指針可以實現計數器位並使用該信息來決定是否需要移動對象到較慢的存儲上。如果將來需要訪問,則讀屏障可以從存儲中檢索到對象。

或者對象可以以壓縮形式保存在內存中,而不是將對象重定位到較慢的存儲層。當請求時,可以通過讀屏障將其解壓並重新分配。

ZGC的狀態在撰寫本文時,ZGC仍然是實驗性的。

您可以使用Java 11 Early Access版本( http://jdk.java.net/11/ )進行測試,但值得指出的是,可能需要一段時間才能解決新版本中的所有問題。對於垃圾收集器來說,從G1發佈到最終支持之間超過三年。

概要

隨着擁有數百GB到數TB RAM的服務器變得越來越普及,Java有效使用該規模堆的能力變得越來越重要。

ZGC是個令人興奮的新垃圾收集器,旨在爲大堆提供非常低的暫停時間。 它通過使用着色指針和讀屏障來實現這一點,這些是Hotspot新近開發的GC技術,併爲未來增加了很多可能性。 ZGC在Java 11中作爲實驗性的功能提供,現在可以使用Early Access 版本試用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章