Oracle即將釋出的全新Java垃圾收集器 ZGC
Java 11的特性集合已經確定,其中包含了一些非常棒的特性。新版本提供了一個全新的垃圾回收器ZGC,它由甲骨文開發,承諾在TB級別的堆上實現非常低的停頓時間。在本文中,我們將介紹甲骨文開發ZGC的動機、ZGC的技術概覽以及ZGC帶來的一些非常令人興奮的可能性。
那麼為什麼要開發ZGC?畢竟Java 10中已經帶有4款久經考驗的垃圾回收器。Hotspot最新的垃圾回收器G1是在2006年推出的。當時最大的AWS例項是m1.small,配備1個vCPU和1.7GB記憶體,而到了今天,AWS提供了x1e.32xlarge例項,配備了128個vCPU和令人難以置信的3,904GB記憶體。ZGC所針對的是這些在未來普遍存在的大容量記憶體:TB級別的堆容量,具有很低的停頓時間(小於10毫秒),對整體應用效能的影響也很小(對吞吐量的影響低於15%)。ZGC所採用的機制也可以在未來進行擴充套件,以支援一些令人興奮的特性,如多層堆(用於熱物件的DRAM和用於低頻訪問物件的NVMe快閃記憶體)或壓縮堆。
GC術語
要了解ZGC在現有垃圾回收器中所處的位置,以及它是如何達到這個位置的,我們先需要先了解一些術語。最基本的GC包括識別出不再使用的記憶體,並將其變為可用的。現代垃圾回收器通常分幾個階段來完成回收過程,如下所示:
- 並行(Parallel)——執行中的JVM包含應用程式執行緒和GC執行緒。在並行階段,會執行多個GC執行緒,也就是說任務被拆分給它們去完成。至於GC執行緒是否可以與正在執行的應用程式執行緒重疊,這個在規範中並沒有特別說明。
- 序列(Serial)——序列階段只有單個GC執行緒在執行。與上面的並行階段一樣,規範中也沒有說明GC執行緒是否可以與當前執行的應用程式執行緒重疊。
- Stop The World(STW)——在這個階段,應用程式執行緒被暫停,讓GC執行緒執行它們的任務。當你遇到GC停頓時,說明虛擬機器進入了STW階段。
- 併發(Concurrent)——在併發階段,GC執行緒可以在執行應用程式執行緒的同時執行自己的任務。併發階段非常複雜,因為應用程式執行緒有可能在GC完成之前將其中斷。
- 增量(Incremental)——在增量階段,它可以執行一段時間,並基於某些條件提前終止,例如時間預算或執行更高優先順序的GC階段。
權衡取捨
需要指出的是,所有這些屬性都存在權衡。例如,並行階段將利用多個GC執行緒來執行任務,但這樣做會導致協調執行緒的開銷。同樣,併發階段不會暫停應用程式執行緒,但可能涉及更多的開銷和複雜性。
ZGC
在瞭解了GC不同階段的屬性後,現在讓我們來探討ZGC的工作原理。ZGC使用了兩項新技術:彩色指標和載入屏障。
指標著色
指標著色是將資訊儲存在指標(或引用)中的一種技術。這是有可能的,因為在64位平臺上(ZGC僅支援64位),指標可以處理比系統實際擁有的記憶體更大的記憶體,因此可以使用多餘的位來儲存狀態。ZGC將堆限制為4TB,需要42位,剩下的22位當中目前已經使用了4位:finalizable、remap、mark0和mark1。
不過,指標著色也存在一個問題,當你想要取消引用指標時,需要做額外的工作,因為你需要遮蔽掉資訊位。SPARC平臺已經為指標遮蔽提供了內建硬體支援,所以這不是什麼問題。但x86平臺還沒有提供類似的支援,所以ZGC團隊針對x86平臺使用了多次對映技術。
多次對映
要了解多對映的工作原理,我們需要先簡要地解釋一下虛擬記憶體和實體記憶體之間的區別。實體記憶體是系統可用的實際記憶體,也就是DRAM晶片的容量。虛擬記憶體是抽象的,對於應用程式來說,它們有自己的實體記憶體試圖(通常是隔離的)。作業系統負責維護虛擬記憶體和實體記憶體之間的對映,通過使用頁表和處理器的記憶體管理單元(MMU)以及轉換後備緩衝區(TLB,用於轉換應用程式的請求地址)來實現。
多次對映技術將不同範圍的虛擬記憶體對映到同一實體記憶體上。在remap、mark0和mark1當中,同一時間點只能有一個為1,因此可以使用三個對映。ZGC原始碼中提供了一個很直觀的圖表(ofollow,noindex" target="_blank">http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hotspot/os_cpu/linux_x86/zGlobals_linux_x86.hpp#l39 )。
載入屏障
載入屏障是一小段程式碼,當應用程式執行緒從堆載入引用時就會執行這段程式碼(即訪問物件的非原始型別欄位):
void printName( Person person ) { String name = person.name;// 將會觸發載入屏障,因為從堆中載入了一個引用 System.out.println(name);// 沒有直接使用載入屏障 }
第一行程式碼是給變數name賦值,這需要跟蹤堆上的person引用,然後再載入name引用。這個時候會觸發載入屏障。第二行程式碼在螢幕上列印name,不會直接觸發載入屏障,因為不需要載入堆引用——name是區域性變數,因此不需要從堆載入引用。不過,System和out,或者println內部可能會觸發其他載入屏障。
這與其他垃圾回收器(例如G1)使用的寫入屏障形成對比。載入屏障的任務是檢查引用的狀態,並在將引用(或者不同的引用)返回給應用程式之前執行一些任務。在ZGC中,它會對載入的引用進行測試,檢視是否設定了某些位,具體取決於當前處於哪個階段。如果引用通過測試,就不執行任何其他操作,如果沒有通過,就會在將引用返回給應用程式之前執行一些特定於當前階段的操作。
標記
在瞭解了這兩項新技術後,現在讓我們來看看ZGC的GC週期。GC週期的第一部分是標記,就是以某種方式查詢並標記應用程式可以訪問到的所有堆物件,換句話說,就是查詢非垃圾物件。
ZGC的標記分為三個階段。第一階段是STW,在這一階段,GC root被標記為存活。GC root類似於區域性變數,應用程式使用它們來訪問堆上的其他物件。從GC root開始遍歷物件圖,如果某些物件無法被訪問到,那麼應用程式也就無法訪問到這些物件,它們就被認為是垃圾。可以從GC root訪問到的物件集被稱為存活集。GC root標記步驟所需要的時間非常短,因為GC root的總量通常相對較少。
標記階段完成後,應用程式恢復執行,而ZGC將開始下一階段,發遍歷物件圖,並標記所有可訪問的物件。在這一階段,載入屏障會檢查所有已載入的引用,看看它們的掩碼是否已經針對這一階段進行過標記,如果尚未標記,就將其新增到待標記佇列。
在完成這一步後,會出現一個短暫的STW階段,它會處理一些邊緣情況,然後整個標記過程就完成了。
重定位
GC週期的下一個主要部分是重定位。重定位就是要移動存活物件,以便釋放部分堆空間。為什麼要移動物件而不是填補空隙?有些GC確實是這樣做的,但這樣會造成不好的後果,即堆分配將變得非常昂貴,因為在分配堆空間時,分配器需要找到放置物件的空閒空間。相反,如果可以釋放大塊記憶體,堆空間分配就會變得很簡單,只需要將指標按照物件所需的記憶體量進行遞增就可以了。
ZGC將堆分成頁,在開始進行重定位時,它會選擇一組需要重新定位的存活物件的頁。在選擇好重定位集後,會出現一次STW停頓,ZGC對重定位集中的物件進行重定位,並重新對映它們對新地址的引用。與之前的STW一樣,停頓時間取決於root的數量以及重定位集與存活集的比率,這個比率通常都很小。它不會隨著堆大小的變化而變化,這與其他大部分垃圾回收器一樣。
移動完root之後,下一階段是進行併發重定位。在這個階段,GC執行緒遍歷重定位集,並重新定位頁中的所有物件。如果應用程式執行緒嘗試載入重定位集中的物件,但這些物件還未被重定位,那麼應用程式執行緒也可以對它們進行重定位,這是通過載入屏障來實現的,如下面的流程圖所示:
這樣可以確保應用程式看到的所有引用都是最新的,並且應用程式不會對正在被重定位的物件做任何操作。
GC執行緒最終會重定位重定位集中的所有物件,不過仍然可能存在一些指向這些物件舊地址的引用。GC會遍歷物件圖,並將所有這些引用重新對映到新的地址上,但這是一個非常昂貴的步驟。所以,這一步被併入到下一個標記階段。在標記期間,如果發現未重新對映的引用,則將其重新對映,並標記為存活。
回顧
試圖單獨理解複雜的垃圾回收器(如ZGC)效能特徵是很困難的,但有一點是很清楚的,我們在文中所提到的GC停頓都與GC root有關,而與存活物件集、堆大小或垃圾物件沒有關係。標記階段的最後一次停頓是一個例外,它是增量進行的,而且如果超過時間預算,GC將恢復到併發標記,直到下一次進行嘗試。
效能
那麼ZGC的效能如何?ZGC的SPECjbb 2015吞吐量資料與Parallel GC(為吞吐量進行過優化)大致相當,平均停頓時間為1毫秒,最長為4毫秒。這與平均停頓時間超過200毫秒的G1和Parallel形成鮮明的對比。
未來的可能性
彩色指標和載入屏障為我們帶來了一些有趣的未來可能性。
多層堆和壓縮
隨著快閃記憶體和非易失性記憶體變得越來越普及,JVM的多層堆將成為可能,在多層堆中,很少被訪問的存活物件將被儲存在較慢的記憶體層中。
我們可以對指標元資料進行擴充套件,加入一些計數器位,並使用這些位資訊來決定是否需要移動物件。在需要使用物件的時候,可以通過載入屏障從相應的記憶體層獲取物件。
或者也可以不將物件重定位到較慢的記憶體層,而是將物件儲存在主記憶體中,不過需要對其進行壓縮。在請求物件時,通過載入屏障解對其進行解壓並分配到堆中。
ZGC的狀態
在撰寫本文時,ZGC還處在實驗階段。讀者可以通過Java 11 Early Access版本(http://jdk.java.net/11/ )來體驗ZGC,但需要指出的是,要解決一個新垃圾回收器存在的所有問題可能需要很長的一段時間。G1從釋出到脫離實驗階段花了至少三年時間。
總結
伺服器擁有數百GB甚至是數TB的記憶體變得越來越普及,Java有效使用記憶體堆的能力變得越來越重要。ZGC是一個令人興奮的新型垃圾回收器,致力於大幅降低大堆垃圾回收的停頓時間。它通過使用彩色指標和載入屏障來實現這一點,它們都是Hotspot新引入的GC技術,並帶來了一些有趣的未來可能性。ZGC將作為Java 11的實驗性垃圾回收器,讀者現在可以通過Java 11 Early Access體驗ZGC。
英文原文:https://www.opsian.com/blog/javas-new-zgc-is-very-exciting/