Java最前沿技術——ZGC

ZGC介紹

ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求極致低延遲的實驗性質的垃圾收集器,它曾經設計目標包括:java

  • 停頓時間不超過10ms;
  • 停頓時間不會隨着堆的大小,或者活躍對象的大小而增長;
  • 支持8MB~4TB級別的堆(將來支持16TB)。

當初,提出這個目標的時候,有不少人都以爲設計者在吹牛逼。算法

但今天看來,這些「吹下的牛逼」都在一個個被實現。安全

基於最新的JDK15來看,「停頓時間不超過10ms」和「支持16TB的堆」這兩個目標已經實現,而且官方明確指出JDK15中的ZGC再也不是實驗性質的垃圾收集器,且建議投入生產了。微信

本文會從ZGC的設計思路出發,講清楚爲什麼ZGC能在低延時場景中的應用中有着如此卓越的表現。併發

核心技術

多重映射

爲了能更好的理解ZGC的內存管理,咱們先看一下這個例子:app

你在你爸爸媽媽眼中是兒子,在你女友眼中是男友。在全世界人面前就是最帥的人。你還有一個名字,但名字也只是你的一個代號,並非你本人。將這個關係畫一張映射圖表示:less

  • 在你爸爸的眼中,你就是兒子;
  • 在你女友的眼中,你就說男友;
  • 站在全世界角度來看,你就說世界上最帥的人;

假如你的名字是全世界惟一的,經過「你的名字」、「你爸爸的兒子」、「你女友的男友」,「世界上最帥的人」最後定位到的都是你本人。性能

如今咱們再來看看ZGC的內存管理。測試

ZGC爲了能高效、靈活地管理內存,實現了兩級內存管理:虛擬內存和物理內存,而且實現了物理內存和虛擬內存的映射關係。這和操做系統中虛擬地址和物理地址設計思路基本一致。優化

當應用程序建立對象時,首先在堆空間申請一個虛擬地址,ZGC同時會爲該對象在Marked0、Marked1和Remapped三個視圖空間分別申請一個虛擬地址,且這三個虛擬地址對應同一個物理地址。

圖中的Marked0、Marked1和Remapped三個視圖是什麼意思呢?

對照上面的例子,這三個視圖分別對應的就是"你爸爸眼中",「你女友的眼中」,「全世界人眼中」。

而三個視圖裏面的地址,都是虛擬地址,對應的是「你爸爸眼中的兒子」,「你女友眼中的男友」......

最後,這些虛地址都能定位到一個物理地址,這個物理地址對應上面例子中的「你本人」。

用一段簡單的Java代碼表示就是這樣的:

<div align="center" width="50%;"> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/992a5261980a481ba2a1294a0276d7d9~tplv-k3u1fbpfcp-zoom-1.image)

在ZGC中這三個空間在同一時間點有且僅有一個空間有效。

爲何這麼設計呢?這就是ZGC的高明之處,利用虛擬空間換時間,這三個空間的切換是由垃圾回收的不一樣階段觸發的,經過限定三個空間在同一時間點有且僅有一個空間有效高效的完成GC過程的併發操做,具體實現會後面講ZGC併發處理算法的部分再詳細描述。

染色指針

在講ZGC併發處理算法以前,還須要補充一個知識點——染色指針。

咱們都知道,以前的垃圾收集器都是把GC信息(標記信息、GC分代年齡..)存在對象頭的Mark Word裏。舉個例子:

若是某我的是個垃圾人,就在這我的的頭上蓋一個「垃圾」的章;若是這我的不是垃圾了,就把這我的頭上的「垃圾」印章洗掉。

而ZGC是這樣作的:

若是某我的是垃圾人。就在這我的的身份證信息裏面標註這我的是個垃圾,之後無論這我的在哪刷身份證,別人都知道他是個垃圾人了。也許哪一天,這我的醒悟了再也不是垃圾人了,就把這我的身份證裏面的「垃圾」標誌去掉。

在這例子中,「這我的」就是一個對象,而「身份證」就是指向這個對象的指針。

ZGC將信息存儲在指針中,這種技術有一個高大上的名字——染色指針(Colored Pointer)。

在64位的機器中,對象指針是64位的。

  • ZGC使用64位地址空間的第0~43位存儲對象地址,2^44 = 16TB,因此ZGC最大支持16TB的堆。
  • 而第44~47位做爲顏色標誌位,Marked0、Marked1和Remapped表明三個視圖標誌位,Finalizable表示這個對象只能經過finalizer才能訪問。
  • 第48~63位固定爲0沒有利用。

讀屏障

讀屏障是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併發處理算法

ZGC併發處理算法利用全局空間視圖的切換和對象地址視圖的切換,結合SATB算法實現了高效的併發。

以上全部的鋪墊,都是爲了講清楚ZGC的併發處理算法,在一些博文上,都說染色指針和讀屏障是ZGC的核心,但都沒有講清楚二者是如何在算法裏面被利用的,我認爲,ZGC的併發處理算法纔是ZGC的核心,染色指針和讀屏障只不過是爲算法服務而已。

ZGC的併發處理算法三個階段的全局視圖切換以下:

  • 初始化階段:ZGC初始化以後,整個內存空間的地址視圖被設置爲Remapped
  • 標記階段:當進入標記階段時的視圖轉變爲Marked0(如下皆簡稱M0)或者Marked1(如下皆簡稱M1)
  • 轉移階段:從標記階段結束進入轉移階段時的視圖再次設置爲Remapped

標記階段

標記階段全局視圖切換到M0視圖。由於應用程序和標記線程併發執行,那麼對象的訪問可能來自標記線程和應用程序線程。

在標記階段結束以後,對象的地址視圖要麼是M0,要麼是Remapped。

  • 若是對象的地址視圖是M0,說明對象是活躍的;
  • 若是對象的地址視圖是Remapped,說明對象是不活躍的,即對象所使用的內存能夠被回收。

當標記階段結束後,ZGC會把全部活躍對象的地址存到對象活躍信息表,活躍對象的地址視圖都是M0。

轉移階段

轉移階段切換到Remapped視圖。由於應用程序和轉移線程也是併發執行,那麼對象的訪問可能來自轉移線程和應用程序線程。

至此,ZGC的一個垃圾回收週期中,併發標記和併發轉移就結束了。

爲什麼要設計M0和M1

咱們提到在標記階段存在兩個地址視圖M0和M1,上面的算法過程顯示只用到了一個地址視圖,爲何設計成兩個?簡單地說是爲了區別前一次標記和當前標記。

ZGC是按照頁面進行部份內存垃圾回收的,也就是說當對象所在的頁面須要回收時,頁面裏面的對象須要被轉移,若是頁面不須要轉移,頁面裏面的對象也就不須要轉移。

如圖,這個對象在第二次GC週期開始的時候,地址視圖仍是M0。若是第二次GC的標記階段還切到M0視圖的話,就不能區分出對象是活躍的,仍是上一次垃圾回收標記過的。這個時候,第二次GC週期的標記階段切到M1視圖的話就能夠區分了,此時這3個地址視圖表明的含義是:

  • M1:本次垃圾回收中識別的活躍對象。

  • M0:前一次垃圾回收的標記階段被標記過的活躍對象,對象在轉移階段未被轉移,可是在本次垃圾回收中被識別爲不活躍對象。

  • Remapped:前一次垃圾回收的轉移階段發生轉移的對象或者是被應用程序線程訪問的對象,可是在本次垃圾回收中被識別爲不活躍對象。

如今,咱們能夠回答「使用地址視圖和染色指針有什麼好處」這個問題了

使用地址視圖和染色指針能夠加快標記和轉移的速度。之前的垃圾回收器經過修改對象頭的標記位來標記GC信息,這是有內存存取訪問的,而ZGC經過地址視圖和染色指針技術,無需任何對象訪問,只須要設置地址中對應的標誌位便可。這就是ZGC在標記和轉移階段速度更快的緣由。

當GC信息再也不存儲在對象頭上時而存在引用指針上時,當肯定一個對象已經無用的時候,能夠當即重用對應的內存空間,這是把GC信息放到對象頭所作不到的。

ZGC步驟

ZGC採用的是標記-複製算法,標記、轉移和重定位階段幾乎都是併發的,ZGC垃圾回收週期以下圖所示:

ZGC只有三個STW階段:初始標記再標記初始轉移

其中,初始標記和初始轉移分別都只須要掃描全部GC Roots,其處理時間和GC Roots的數量成正比,通常狀況耗時很是短;

再標記階段STW時間很短,最多1ms,超過1ms則再次進入併發標記階段。即,ZGC幾乎全部暫停都只依賴於GC Roots集合大小,停頓時間不會隨着堆的大小或者活躍對象的大小而增長。與ZGC對比,G1的轉移階段徹底STW的,且停頓時間隨存活對象的大小增長而增長。

ZGC的發展

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背後的設計思路。

但願你能有所收穫!

談談生活

談談本身對疫情的見解。

個人見解是:注意安全,沒必要過於擔心

前段時間,由於我室友的公司所在的大樓的一個公司的一個員工去韓國核酸檢測爲陽性,因此我居家隔離了14天,從12月24號到1月6號。

被隔離的第一天,個人北京健康碼就被標註了「居家觀察」,社區工做人員第一時間就在門口裝了攝像頭,整個過程不容許出門,不容許和人接觸。

這14天裏,吃的東西都是點的外賣,爲了避免直接接觸,都是外賣放門口,等外賣小哥走了再開門取。

天天都會由社區人員上門來收取生活垃圾,而後集中處理。上午和下午都須要在社區服務羣裏面彙報本身的體溫和情況,有什麼須要和幫助也能夠在羣裏說,社區人員也會立刻迴應。

14天一共作了三次核酸檢測,每次檢測的時候都會給你發一個N95的口罩。最後一次作核酸檢測那天北京-20度,爲了避免讓咱們感冒,還有專車接送,一批一批的送到指定點作核酸檢測。

整個過程管控的很是細心,社區人員方方面面都想得很是周到。因此,在國家這種管控力度下,我相信疫情不會繼續擴散的。

同時也但願你們作好防禦措施,保護好本身,歡歡喜喜回家過個年。做爲打工人的我是很是想回家過年的。回家過年本就是多年的傳承,就像候鳥南飛同樣。固然特殊時期能夠特殊對待,但條件容許的話仍是回家吧,看看家裏的老人,見見各奔東西的朋友,一年就這幾天而已。

寫在最後

爲了對每一篇發出去的文章負責,力求準確,我通常是參考官方文檔和業界權威的書籍,有些時候,還須要看一些論文,看一部分源代碼。而官方文檔和論文通常都是英文,對於一個英語四級只考了456分的人來講,很是艱難,整個過程都是谷歌翻譯和有道詞典陪伴着個人。由於一些專業術語翻譯的不夠準確,還須要英文和翻譯對照慢慢理解。

但即便這樣,也不免會有紕漏,若是你發現了,歡迎提出,我會對其修正。

你的正反饋對我來講很是重要,點個贊,點個關注都是對我最大的支持!

若是你有什麼想和我交流的,能夠關注個人微信公衆號「CoderW」,很是歡迎並感謝您的關注!

謝謝您的閱讀,咱們下期再見!

參考資料

相關文章
相關標籤/搜索