ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求極致低延遲的實驗性質的垃圾收集器,它曾經設計目標包括:java
當初,提出這個目標的時候,有不少人都以爲設計者在吹牛逼。面試
但今天看來,這些「吹下的牛逼」都在一個個被實現。算法
基於最新的JDK15來看,「停頓時間不超過10ms」和「支持16TB的堆」這兩個目標已經實現,而且官方明確指出JDK15中的ZGC再也不是實驗性質的垃圾收集器,且建議投入生產了。併發
ZGC已經熟了,面試題還會遠嗎?app
本文會從ZGC的設計思路出發,講清楚爲什麼ZGC能在低延時場景中的應用中有着如此卓越的表現。less
爲了能更好的理解ZGC的內存管理,咱們先看一下這個例子:性能
你在你爸爸媽媽眼中是兒子,在你女友眼中是男友。在全世界人面前就是最帥的人。你還有一個名字,但名字也只是你的一個代號,並非你本人。將這個關係畫一張映射圖表示:測試
假如你的名字是全世界惟一的,經過「你的名字」、「你爸爸的兒子」、「你女友的男友」,「世界上最帥的人」最後定位到的都是你本人。優化
如今咱們再來看看ZGC的內存管理。spa
ZGC爲了能高效、靈活地管理內存,實現了兩級內存管理:虛擬內存和物理內存,而且實現了物理內存和虛擬內存的映射關係。這和操做系統中虛擬地址和物理地址設計思路基本一致。
當應用程序建立對象時,首先在堆空間申請一個虛擬地址,ZGC同時會爲該對象在Marked0、Marked1和Remapped三個視圖空間分別申請一個虛擬地址,且這三個虛擬地址對應同一個物理地址。
圖中的Marked0、Marked1和Remapped三個視圖是什麼意思呢?
對照上面的例子,這三個視圖分別對應的就是"你爸爸眼中",「你女友的眼中」,「全世界人眼中」。
而三個視圖裏面的地址,都是虛擬地址,對應的是「你爸爸眼中的兒子」,「你女友眼中的男友」......
最後,這些虛地址都能映射到同一個物理地址,這個物理地址對應上面例子中的「你本人」。
用一段簡單的Java代碼表示這種關係:
在ZGC中這三個空間在同一時間點有且僅有一個空間有效。
爲何這麼設計呢?這就是ZGC的高明之處,利用虛擬空間換時間,這三個空間的切換是由垃圾回收的不一樣階段觸發的,經過限定三個空間在同一時間點有且僅有一個空間有效高效的完成GC過程的併發操做,具體實現會在後面講ZGC併發處理算法的部分再詳細描述。
在講ZGC併發處理算法以前,還須要補充一個知識點——染色指針。
咱們都知道,以前的垃圾收集器都是把GC信息(標記信息、GC分代年齡..)存在對象頭的Mark Word裏。舉個例子:
若是某我的是個垃圾人,就在這我的的頭上蓋一個「垃圾」的章;若是這我的不是垃圾了,就把這我的頭上的「垃圾」印章洗掉。
而ZGC是這樣作的:
若是某我的是垃圾人。就在這我的的身份證信息裏面標註這我的是個垃圾,之後無論這我的在哪刷身份證,別人都知道他是個垃圾人了。也許哪一天,這我的醒悟了再也不是垃圾人了,就把這我的身份證裏面的「垃圾」標誌去掉。
在這例子中,「這我的」就是一個對象,而「身份證」就是指向這個對象的指針。
ZGC將信息存儲在指針中,這種技術有一個高大上的名字——染色指針(Colored Pointer)。
在64位的機器中,對象指針是64位的。
讀屏障是JVM嚮應用代碼插入一小段代碼的技術。當應用線程從堆中讀取對象引用時,就會執行這段代碼。千萬不要把這個讀屏障和Java內存模型裏面的讀屏障搞混了,二者根本不是同一個東西,ZGC中的讀屏障更像是一種AOP技術,在字節碼層面或者編譯代碼層面給讀操做增長一個額外的處理。
讀屏障實例:
Object o = obj.FieldA // 從堆中讀取對象引用,須要加入讀屏障 <load barrier needed here> Object p = o // 無需加入讀屏障,由於不是從堆中讀取引用 o.dosomething() // 無需加入讀屏障,由於不是從堆中讀取引用 int i = obj.FieldB // 無需加入讀屏障,由於不是對象引用
ZGC中讀屏障的代碼做用:
GC線程和應用線程是併發執行的,因此存在應用線程去A對象內部的引用所指向的對象B的時候,這個對象B正在被GC線程移動或者其餘操做,加上讀屏障以後,應用線程會去探測對象B是否被GC線程操做,而後等待操做完成再讀取對象,確保數據的準確性。具體的探測和操做步驟以下:
這樣會影響程序的性能嗎?
會。據測試,最多百分之4的性能損耗。但這是ZGC併發轉移的基礎,爲了下降STW,設計者認爲這點犧牲是可接受的。
ZGC併發處理算法利用全局空間視圖的切換和對象地址視圖的切換,結合SATB算法實現了高效的併發。
以上全部的鋪墊,都是爲了講清楚ZGC的併發處理算法,在一些博文上,都說染色指針和讀屏障是ZGC的核心,但都沒有講清楚二者是如何在算法裏面被利用的,我認爲,ZGC的併發處理算法纔是ZGC的核心,染色指針和讀屏障只不過是爲算法服務而已。
ZGC的併發處理算法三個階段的全局視圖切換以下:
標記階段全局視圖切換到M0視圖。由於應用程序和標記線程併發執行,那麼對象的訪問可能來自標記線程和應用程序線程。
在標記階段結束以後,對象的地址視圖要麼是M0,要麼是Remapped。
當標記階段結束後,ZGC會把全部活躍對象的地址存到對象活躍信息表,活躍對象的地址視圖都是M0。
轉移階段切換到Remapped視圖。由於應用程序和轉移線程也是併發執行,那麼對象的訪問可能來自轉移線程和應用程序線程。
至此,ZGC的一個垃圾回收週期中,併發標記和併發轉移就結束了。
咱們提到在標記階段存在兩個地址視圖M0和M1,上面的算法過程顯示只用到了一個地址視圖,爲何設計成兩個?簡單地說是爲了區別前一次標記和當前標記。
ZGC是按照頁面進行部份內存垃圾回收的,也就是說當對象所在的頁面須要回收時,頁面裏面的對象須要被轉移,若是頁面不須要轉移,頁面裏面的對象也就不須要轉移。
如圖,這個對象在第二次GC週期開始的時候,地址視圖仍是M0。若是第二次GC的標記階段還切到M0視圖的話,就不能區分出對象是活躍的,仍是上一次垃圾回收標記過的。這個時候,第二次GC週期的標記階段切到M1視圖的話就能夠區分了,此時這3個地址視圖表明的含義是:
如今,咱們能夠回答「使用地址視圖和染色指針有什麼好處」這個問題了
使用地址視圖和染色指針能夠加快標記和轉移的速度。之前的垃圾回收器經過修改對象頭的標記位來標記GC信息,這是有內存存取訪問的,而ZGC經過地址視圖和染色指針技術,無需任何對象訪問,只須要設置地址中對應的標誌位便可。這就是ZGC在標記和轉移階段速度更快的緣由。
當GC信息再也不存儲在對象頭上時而存在引用指針上時,當肯定一個對象已經無用的時候,能夠當即重用對應的內存空間,這是把GC信息放到對象頭所作不到的。
ZGC採用的是標記-複製算法,標記、轉移和重定位階段幾乎都是併發的,ZGC垃圾回收週期以下圖所示:
ZGC只有三個STW階段:初始標記,再標記,初始轉移。
其中,初始標記和初始轉移分別都只須要掃描全部GC Roots,其處理時間和GC Roots的數量成正比,通常狀況耗時很是短;
再標記階段STW時間很短,最多1ms,超過1ms則再次進入併發標記階段。即,ZGC幾乎全部暫停都只依賴於GC Roots集合大小,停頓時間不會隨着堆的大小或者活躍對象的大小而增長。與ZGC對比,G1的轉移階段徹底STW的,且停頓時間隨存活對象的大小增長而增長。
ZGC誕生於JDK11,通過不斷的完善,JDK15中的ZGC已經再也不是實驗性質的了。
從只支持Linux/x64,到如今支持多平臺;從不支持指針壓縮,到支持壓縮類指針.....
在JDK16,ZGC將支持併發線程棧掃描(Concurrent Thread Stack Scanning),根據SPECjbb2015測試結果,實現併發線程棧掃描以後,ZGC的STW時間又能下降一個數量級,停頓時間將進入毫秒時代。
ZGC已然是一款優秀的垃圾收集器了,它借鑑了Pauseless GC,也彷佛在朝着C4 GC的方向發展——引入分代思想。
Oracle的努力,讓咱們開發者看到了商用級別的GC「飛入尋常百姓家」的但願,隨着JDK的發展,我相信在將來的某一天,JVM調優這種反人類的操做將不復存在,底層的GC會自適應各類狀況自動優化。
ZGC確實是Java的最前沿的技術,但在G1都沒有普及的今天,談論ZGC彷佛爲時過早。但也許咱們探討的不是ZGC,而是ZGC背後的設計思路。
但願你能有所收穫!
爲了對每一篇發出去的文章負責,力求準確,我通常是參考官方文檔和業界權威的書籍,有些時候,還須要看一些論文,看一部分源代碼。而官方文檔和論文通常都是英文,對於一個英語四級只考了456分的人來講,很是艱難,整個過程都是谷歌翻譯和有道詞典陪伴着個人。由於一些專業術語翻譯的不夠準確,還須要英文和翻譯對照慢慢理解。
但即便這樣,也不免會有紕漏,若是你發現了,歡迎提出,我會對其修正。
你的正反饋對我來講很是重要,點個贊,點個再看,點個關注都是對我最大的支持!
謝謝您的閱讀,咱們下期再見!