ZGC 深入理解

概述

G1作爲新一代成熟的垃圾回收器尚未得到廣泛使用,新一代的垃圾回收器ZGC在JDK 11中引入,ZGC是2017年Oracle公司貢獻給OpenJDK社區的,正式成爲OpenJDK的開源項目,也就是JEP 333,目前它被明確地標記爲實驗性質(意味着還不成熟)。新一代的垃圾回收器一經發布,雖然尚不成熟,但是仍然阻擋不了衆多Java程序員對它的追捧。ZGC是爲了解決G1的不足,我們先看一下G1有哪些不足。

G1的目標是在可控的停頓時間內完成垃圾回收,所以進行了分區設計,在回收時採用部分內存回收(在YGC時會回收所有新生代分區,在混合回收時會回收所有的新生代分區和部分老生代分區),支持的內存也可以達到幾十個GB或者上百個GB。爲了進行部分回收,G1實現了RSet管理對象的引用關係。基於G1設計上的特點,導致存在以下問題:

  1. 停頓時間過長,通常G1的停頓時間要達到幾十到幾百毫秒;這個數字其實已經非常小了,但是我們知道垃圾回收發生導致應用程序在這幾十或者幾百毫秒中不能提供服務,在某些場景中,特別是對用戶體驗有較高要求的情況下不能滿足實際需求。
  2. 內存利用率不高,通常引用關係的處理需要額外消耗內存,一般佔整個內存的1%~20%左右。
  3. 支持的內存空間有限,不適用於超大內存的系統,特別是在內存容量高於100GB的系統中,會因內存過大而導致停頓時間增長。

ZGC作爲新一代的垃圾回收器,在設計之初就定義了三大目標:支持TB級內存,停頓時間控制在10ms之內,對程序吞吐量影響小於15%。實際上目前ZGC已經滿足設計之初定義的目標,最大支持4TB堆空間,依據實際測試的情況來看,停頓時間通常都在10ms以下,並且垃圾回收所引起的暫停時間並不會隨着內存的增大而延長。下面我們看一看ZGC是如何滿足設計目標的。

簡單地說,就是ZGC把一切能併發處理的工作都併發執行。在這裏再強調一下JVM中“並行”和“併發”這兩個詞:並行指多個垃圾回收相關線程在操作系統之上併發地運行,強調的是隻有垃圾回收線程工作,Java應用程序都暫停執行,因此並行線程執行的時候一定發生了STW;併發指如果啓動了多個線程,那麼與垃圾回收相關的線程併發地運行,同時這些線程會和Java應用程序併發地運行。所有線程都由操作系統調度,交替執行。

除了併發執行這個顯著特點之外,ZGC還有以下特點:

  • 不分代的垃圾回收器,即垃圾回收時對全量內存進行標記,但是回收時僅針對部分內存回收,優先回收垃圾比較多的頁面。
  • 僅支持Linux 64位系統,不支持32位平臺。
  • 不支持使用壓縮指針。
  • 內存分區管理,且支持不同的分區粒度,在ZGC中分區稱爲頁面(page),有小頁面、中頁面、大頁面3種。
  • 具有顏色指針(color pointer),通過設計不同的標記位區分不同的虛擬空間,而這些不同標記位指示的不同虛擬空間通過mmap映射在同一物理地址;顏色指針能夠快速實現併發標記、轉移和重定位。
  • 設計了讀屏障,實現了併發標記和併發轉移的處理。
  • 支持NUMA,儘量把對象分配在訪問速度比較快的地方。

ZGC內存管理

對象的分配直接關係到內存的使用效率、垃圾回收的效率,不同的分配策略也會影響對象的分配速度,從而影響應用程序的運行。

ZGC爲了支持太字節(TB)級內存,設計了基於頁面(page)的分頁管理(類似於G1的分區Region);爲了能夠快速地進行併發標記和併發移動,對內存空間重新進行了劃分,這就是ZGC中新引入的Color Pointers;同時ZGC爲了能更加高效地管理內存,設計了物理內存和虛擬內存兩級內存管理。

爲了能清晰地瞭解ZGC內存管理,在本章中,我們先介紹操作系統的虛擬內存和物理內存;隨後介紹了ZGC的內存管理,主要包括多視圖映射、NUMA支持和ZGC的兩級內存管理;最後介紹了ZGC的對象分配,包括對象的快速分配和慢速分配、頁面的分配。

  • 着色指針還不是很瞭解 本章略

垃圾回收觸發的時機

本節主要介紹ZGC中的這些消息是何時觸發的。
ZGC中,爲了實現更高的性能,儘量避免進行同步垃圾回收,也就是說盡量避免觸發同步垃圾回收的消息。ZGC中觸發同步消息的場景也比較少,總體以觸發異步消息爲主。異步消息主要由ZDirector根據規則判斷是否可以觸發,在ZDirector流程圖(見圖3-3)中介紹了ZDirector有4種觸發規則,本節主要介紹這4種規則是如何觸發的,最後還會簡要介紹其他的垃圾回收消息是如何觸發的。

  • 基於固定時間間隔觸發

ZDirector提供的第一個規則就是基於固定時間間隔觸發垃圾回收。這個規則的目的非常簡單,就是希望ZGC的垃圾回收器以固定的頻率觸發。在這一些場景中非常有用,例如我們的應用程序在晚上請求量比較低的情況下運行了很長時間,但是ZGC不滿足其他垃圾回收器的觸發條件,所以一直不會觸發垃圾回收,這通常沒什麼問題,如果在早上某一個時間點開始請求暴增,這可能導致內存使用也暴增,而垃圾回收器來不及回收垃圾對象,將降低應用系統的吞吐量。所以ZGC提供了基於固定時間間隔觸發垃圾回收的規則。
這個規則的實現也非常簡單,就是判斷前一次垃圾回收結束到當前時間是否超過時間間隔的閾值,如果超過,則觸發垃圾回收,如果不滿足,則直接返回。
需要說明的是,時間間隔由一個參數ZCollectionInterval來控制,這個參數的默認值爲0,表示不需要觸發垃圾回收。
實際工作中,可以根據場景設置該參數。

  • 預熱規則觸發

ZDirector提供的第二個規則是預熱啓動垃圾回收。爲什麼設計這一規則?設計這一規則的目的是當JVM剛啓動時,還沒有足夠的數據來主動觸發垃圾回收的啓動,所以設置了預熱規則。
預熱規則指的是JVM啓動後,當發現堆空間使用率達到10%、20%和30%時,會主動地觸發垃圾回收。ZGC設計前3次垃圾回收可由預熱規則觸發,也就是說當垃圾回收觸發(無論是由預熱規則,還是主動觸發垃圾回收)的次數超過3次時,預熱規則將不再生效。

  • 根據分配速率

ZDirector提供的第三個規則是根據分配速率來預測是否能觸發垃圾回收。這一規則設計的思路是:
1)收集數據:在程序運行時,收集過去一段時間內垃圾回收發生的次數和執行的時間、內存分配的速率memratio和當前空閒內存的大小memfree。
2)計算:根據過去垃圾回收發生的情況預測下一次垃圾回收發生的時間timegc,按照內存分配的速率預測空閒內存能支撐應用程序運行的實際timeoom,例如timeoom = memfree / memratio。
3)設計規則:如當timeoom小於timegc(垃圾回收的時間),則可以啓動垃圾回收。這個規則的含義是如果從現在起到OOM發生前開始執行垃圾回收,剛好在OOM發生前完成垃圾回收的動作,從而避免OOM。在ZGC中ZDirector是週期運行的,所以在計算時還應該把OOM的時間減去採樣週期的時間,採樣週期記爲timeinterval,則規則爲timeoom < timegc + timeinterval時觸發垃圾回收。
那麼最主要的任務就變成了如何預測下一次垃圾回收時間timegc和內存分配的速率memratio(因爲memfree是已知數據,無須額外處理)。
我們以預測垃圾回收時間timegc爲例來看看如何預測。最簡單也最直觀的思路是,根據已經發生的垃圾回收所使用的時間來預測下一次垃圾回收可能花費的時間。這裏提供幾種思路:
1)收集過去一段時間內垃圾回收發生的次數和時間,取過去N次垃圾回收的平均時間作爲下一次垃圾回收的預測時間;這一方法最爲直觀,但是準確度可能有待提高。
2)收集過去一段時間內垃圾回收發生的次數和時間,建立一個邏輯迴歸模型,從而預測下一次垃圾回收的預測時間;這一方法雖然比第一種方法有改進,根據垃圾回收的趨勢來預測下一次垃圾回收的時間,但這一方法最大的問題是邏輯迴歸模型太簡單,實際上如果我們能提供更多的輸入,比如應用程序使用內存的情況、線程數等建立動態模型,這應該是一個非常好的方法。
3)使用衰減平均時間來預測下一次垃圾回收花費的時間。衰減平均方法實際上是第一種方法和第二種方法組合後的一種簡化實現。它是一種簡單的數學方法,用來計算一組數據的平均值,但是在計算平均值的時候最新的數據有更高的權重,即強調近期數據對結果的影響。
4)直接採用已經成熟的模型來預測下一次垃圾回收時間。ZGC中主要是基於正態分佈來預測。

從統計角度來說,當數據樣本足夠大的時候(比如樣本個數大於30個時),使用正態分佈比較準確;當樣本個數不多時,使用t分佈效果比較好。在上述代碼中實際上修正了真正的置信區間,使得置信度更高。如果讀者有興趣,可以實現t分佈,並驗證t分佈和正態分佈預測的準確度。

  • 主動觸發

ZDirector提供的第四個規則是主動觸發規則,該規則是爲了應用程序在吞吐量下降的情況下,當滿足一定條件時,還可以執行垃圾回收。這裏滿足一定條件指的是:
1)從上一次垃圾回收完成到當前時間,應用程序新增使用的內存達到堆空間的10%。
2)從上一次垃圾回收完成到當前時間已經過去了5min,記爲timeelapsed。
如果這兩個條件同時滿足,預測垃圾回收時間爲timegc,定義規則:如果numgc * timegc < timeelapsed,則觸發垃圾回收。其中numgc是ZGC設計的常量,假設應用程序的吞吐率從50%下降到1%,需要觸發一次垃圾回收。
這個規則實際上是爲了彌補程序吞吐率驟降且長時間不執行垃圾回收而引入的。有一個診斷參數ZProactive來控制是否開啓和關閉主動規則,默認值是true,即默認打開主動觸發規則。
實際上這個規則和第一個規則(基於固定時間間隔規則)在某些場景中有一定的重複,第一個規則只強調時間間隔,本規則除了考慮時間之外還會考慮內存的增長和吞吐率下降的快慢程度。

  • 阻塞內存分配請求觸發

阻塞內存分配由參數ZStallOnOutOfMemory控制,當參數ZStallOnOutOfMemory爲true時進行阻塞分配,如果不能成功分配內存,則觸發阻塞內存分配。
該觸發請求是異步消息,並非同步消息。我們在2.3.2節中提到,頁面阻塞分配會觸發垃圾回收,直到垃圾回收完成併成功分配頁面爲止。因爲是異步消息,所以頁面阻塞分配請求需要額外的實現等待成功分配的功能,其實非常簡單,可以通過一個循環來實現。
那爲什麼ZGC不把阻塞內存分配實現成同步消息,而是通過異步消息加上循環的方式?
原因在於同步消息請求的線程在發出同步消息後是通過通知等待機制完成的,通知等待機制通常會讓出CPU,而頁面阻塞分配採用異步消息加上循環的方式,不會讓出CPU,在循環中判斷垃圾回收是否完成,如果完成,則繼續向下執行,這樣的設計可以減少頁面分配時因線程調度帶來的額外開銷。從這一點也可以看出,設計一款優秀的軟件,需要從每一個細節出發,並仔細斟酌。

  • 外部觸發

外部觸發是指在Java代碼中顯式地調用System.gc()函數,在JVM執行該函數時,會觸發垃圾回收。該觸發請求是從用戶代碼主動觸發的,從編程角度來看,說明程序員認爲此時需要進行垃圾回收(當然首先是程序員正確使用System.gc()函數),所以ZGC把該觸發規則設計爲同步請求,只有在執行完垃圾回收後,才能進行後續代碼的執行。

  • 元數據分配觸發

元數據分配失敗時,ZGC會嘗試進行垃圾回收以確保元數據能正確分配。
異步垃圾回收後會嘗試是否可以分配元數據對象空間,如果不能,將嘗試進行同步垃圾回後可以分配元數據對象空間,如果還不成功,則嘗試擴展元數據空間,再分配成功則返回內存空間,不成功則返回NULL。

回收過程

  • (STW)Pause Mark Start,開始標記,這個階段只會標記(Mark0)由root引用的object,組成Root Set
  • Concurrent Mark,併發標記,從Root Set出發,併發遍歷Root Set object的引用鏈並標記(Mark1)
  • (STW)Pause Mark End,檢查是否已經併發標記完成,如果不是,需要進行多一次Concurrent Mark
  • Concurrent Process Non-Strong References,併發處理弱引用
  • Concurrent Reset Relocation Set
  • Concurrent Destroy Detached Pages
  • Concurrent Select Relocation Set,併發選擇Relocation Set
  • Concurrent Prepare Relocation Set,併發預處理Relocation Set
  • (STW)Pause Relocate Start,開始轉移對象,依然是遍歷root引用
  • Concurrent Relocate,併發轉移,將需要回收的Page裏的對象轉移到Relocation Set,然後回收Page給系統重新利用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章