Java 11最近已發佈,包含一些很是棒的功能。該版本包含一個全新的垃圾收集器ZGC,它由Oracle開發,承諾在數TB的堆上具備很是低的暫停時間。在本文中,咱們將介紹新GC的動機,技術概述以及ZGC開啓的一些很是使人興奮的可能性。java
那麼爲何須要新的GC呢?Java 10已經搭載了四艘通過多年實戰測試且幾乎能夠無限調優。爲了正確看待這一點,G1最新的Hotspot GC是在2006年推出的。當時最大的AWS實例是最初的m1.small包裝1 vCPU和1.7GB內存,今天AWS很樂意租給你一個x1e.32x大型,128個vCPU,使人難以置信的3,904GB內存的服務器。ZGC的設計針對這些容量廣泛存在的將來:多TB容量,暫停時間低(<10ms),對總體應用性能有影響(吞吐量 GC術語linux
爲了理解ZGC在哪裏適合現有的收集器,以及它如何可以作到這一點,咱們首先須要回顧一些術語。垃圾收集最基本的工做是肯定再也不使用的內存,並使其能夠重用。現代收藏家是分幾個階段進行這一過程的,他們每每被描述爲:服務器
固有的權衡取捨併發
值得指出的是,全部這些屬性都須要權衡利弊。例如,並行階段將利用多個gc線程來執行工做,但這樣作會致使線程之間協調的開銷。一樣,併發階段不會暫停應用程序線程,但可能涉及更多的開銷和複雜性,以處理應用程序線程同時使其工做無效。ide
ZGC性能
如今咱們瞭解不一樣gc階段的屬性,讓咱們探討ZGC的工做原理。爲了實現其目標,ZGC使用了Hotspot Garbage Collectors的兩種新技術:彩色指針和負載障礙。測試
指針着色優化
指針着色是一種將信息存儲在指針(或Java術語,引用)自己中的技術。這是可能的,由於在64位平臺上(ZGC僅爲64位),指針能夠處理比系統實際擁有的內存更多的內存,所以可使用其餘一些位來存儲狀態。ZGC將本身限制在須要42位的4Tb堆中,只留下22位可能的狀態,目前它使用4位:finalizable、remap、mark0和mark1。咱們稍後會解釋它們的用途。spa
指針着色的一個問題是,當您須要取消引用指針時,它能夠建立額外的工做,由於您須要屏蔽掉信息位。像SPARC這樣的平臺有內置硬件支持指針屏蔽因此它不是問題,但對於x86,ZGC團隊使用了一個簡潔的多映射技巧。操作系統
多重映射
要了解多映射的工做原理,咱們須要簡要解釋虛擬和物理內存之間的區別。物理內存是系統可用的實際內存,一般是安裝的DRAM芯片的容量。虛擬內存是抽象,意味着應用程序有本身的(一般是隔離的)視圖到物理內存。操做系統負責維護虛擬內存和物理內存範圍之間的映射,它經過使用頁表和處理器的內存管理單元(MMU)和轉換後備緩衝區(TLB)來實現這一點,後者轉換應用程序請求的地址。
多映射涉及將不一樣範圍的虛擬內存映射到同一物理內存。因爲設計只有一個重映射,mark0和MARK-1能夠在任什麼時候間點爲1,這是可能的三個映射作到這一點。ZGC源代碼中有一個很好的圖表。
負載障礙
負載障礙是每當應用程序線程從堆加載引用時運行的代碼片斷(即訪問對象上的非原始字段):
void printName( Person person ) {
String name = person.name; // would trigger the load barrier
// because we’ve loaded a reference
// from the heap
System.out.println(name); // no load barrier directly
}
複製代碼
在上面的代碼中,分配名稱的行包括跟隨對堆上對象數據的person引用,而後將引用加載到它包含的名稱。此時觸發負載屏障。觸發打印到屏幕的第二行不會直接致使加載障礙觸發,由於沒有來自堆的引用加載 - 名稱是局部變量,所以沒有從堆加載引用。可是,引用System和out,或者println內部可能會觸發其餘負載障礙。這與其餘GC使用的寫屏障造成對比,例如G1。加載屏障的工做是檢查引用的狀態,並在將引用(或者甚至是不一樣的引用)返回給應用程序以前執行一些工做。在ZGC中,它經過測試加載的引用來執行此任務,以查看是否設置了某些位,具體取決於當前階段。若是引用經過測試,則不執行任何其餘工做,若是失敗,則在將引用返回給應用程序以前執行某些特定於階段的任務。
Marking
如今咱們瞭解了這兩種技術是什麼,讓咱們看看ZGC GC循環。循環的第一部分是標記。標記涉及到以某種方式查找和標記全部堆對象,這些對象能夠被正在運行的應用程序訪問,換句話說,查找不是垃圾的對象。
ZGC的標記分爲三個階段。第一個階段是Stop The World階段,在這個階段中,GC的根被標記爲存在。GC根相似於局部變量,應用程序可使用它訪問堆上的其餘對象。若是對象不能經過從根開始的對象圖遍從來訪問,那麼應用程序就沒法訪問它,它被認爲是垃圾。根中可訪問的對象集稱爲活動集。GC根標記步驟很是短,由於根的總數一般相對較小。
一旦該階段完成,應用程序將繼續,ZGC將開始下一個階段,該階段將同時遍歷對象圖並標記全部可訪問對象。在此階段,load barrier測試全部裝載的引用,它將根據一個掩碼測試它們是否已經被標記,若是一個引用尚未被標記,那麼它將被添加到一個隊列中以進行標記。
在遍歷完成以後,會有一個最後的,簡短的,Stop The World階段,它處理一些邊緣狀況(咱們暫時忽略它),而後標記完成。
Relocation
GC循環的下一個主要部分是從新定位。重定位包括移動活動對象,以便釋放堆的各個部分。爲何要移動對象而不僅是填補空白?一些GCs確實這樣作了,但它的不幸後果是,分配變得更加昂貴,由於當須要分配時,分配程序須要找到放置對象的空閒空間。相反,若是能夠釋放大量內存,那麼分配就會簡單地按照對象所需的內存數量遞增(或「碰撞」)指針。
ZGC將堆劃分爲頁面,在此階段的開始,它會同時選擇一組頁面,這些頁面的活動對象須要從新定位。當選擇重定位集時,有一個Stop(Stop The World),ZGC將重定位重定位集中做爲根(局部變量等)引用的任何對象,並將其引用從新映射到新位置。與前面的Stop the World步驟同樣,這裏所涉及的暫停時間僅取決於根的數量和重定位集的大小與活動對象的總大小的比例,而這一般是至關小的。它不像許多收集器那樣根據堆的總體大小進行縮放。
在移動任何須要的根以後,下一個階段是併發從新定位。在此階段,GC線程遍歷重定位集並從新定位它所包含的頁面中的全部對象。應用程序線程也能夠從新定位重定位集中的對象,若是它們試圖在GC從新定位對象以前加載它們,那麼這能夠經過負載屏障(從堆中加載引用時觸發)實現,詳見如下流程圖:
這可確保應用程序看到的全部引用都已更新,而且應用程序不可能對同時重定位的對象進行操做。
GC線程最終將重定位重定位集中的全部對象,可能仍有引用指向這些對象的舊位置。GC能夠遍歷對象圖並從新映射全部對其新位置的引用,但這是一個昂貴的步驟。相反,這與下一個標記階段相結合。在步行期間,若是發現未從新映射引用,則將其從新映射,而後標記爲活動。
Recap
試圖單獨理解複雜垃圾收集器(如ZGC)的性能特徵是很困難的,但從前面的部分能夠清楚地看出,咱們所涵蓋的幾乎全部暫停都涉及依賴於GC根集合的工做,而不是實時的對象集,堆大小或垃圾。處理標記終止的標記階段的最後一次暫停是一個例外,可是是增量的,若是超過期間預算直到再次嘗試,GC將恢復爲併發標記。
性能
那它是如何表現的?Stefan Karlsson和Per Liden在今年早些時候的Jfokus演講中給出了一些初步數字。ZGC的SPECjbb 2015吞吐量數據與Parallel GC(優化吞吐量)大體至關,但平均暫停時間爲1ms,最長爲4ms。這與平均暫停時間超過200毫秒的G1和平行相反。
然而,垃圾收集器是複雜的野獸,基準測試結果可能沒法推廣到真實世界的性能。咱們期待本身測試ZGC,以瞭解它的性能如何因工做量而異。