導讀: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):
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,以瞭解它的效能如何因工作負載而異。
未來的可能性
著色指標和讀屏障提供了一些有趣的可能。
多層堆和壓縮
隨著快閃記憶體和非易失性儲存器變得越來越普遍,一種可能是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 版本試用。
更多資源
如前所述,Stefan Karlsson和Per Liden的Jfokus演講絕對值得一看。參考:https://www.youtube.com/watch?v=tShc0dyFtgw
DominikInführ還撰寫了一篇關於ZGC中一些底層細節的好文章 ,並附有原始碼連結。
原文地址:
https://medium.com/airbnb-engineering/nebula-as-a-storage-platform-to-build-airbnbs-search-backends-ecc577b05f06
本文作者Sadiq Jaffer & Richard Warburton,由方圓翻譯。轉載本文請註明出處,歡迎更多小夥伴加入翻譯及投稿文章的行列,詳情請戳公眾號選單「聯絡我們」。
參考閱讀:
技術原創及架構實踐文章,歡迎透過公眾號選單「聯絡我們」進行投稿。轉載請註明來自高可用架構「ArchNotes」微信公眾號及包含以下二維碼。
高可用架構
改變網際網路的構建方式
長按二維碼 關註「高可用架構」公眾號