天天看點

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

新一代垃圾回收器ZGC的探索與實踐

2020年08月06日 作者: 王東 王偉 文章連結 12996字 26分鐘閱讀

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延遲垃圾回收器,它的設計目标包括:

  • 停頓時間不超過10ms;
  • 停頓時間不會随着堆的大小,或者活躍對象的大小而增加;
  • 支援8MB~4TB級别的堆(未來支援16TB)。

從設計目标來看,我們知道ZGC适用于大記憶體低延遲服務的記憶體管理和回收。本文主要介紹ZGC在低延時場景中的應用和卓越表現,文章内容主要分為四部分:

  • GC之痛:介紹實際業務中遇到的GC痛點,并分析CMS收集器和G1收集器停頓時間瓶頸;
  • ZGC原理:分析ZGC停頓時間比G1或CMS更短的本質原因,以及背後的技術原理;
  • ZGC調優實踐:重點分享對ZGC調優的了解,并分析若幹個實際調優案例;
  • 更新ZGC效果:展示在生産環境應用ZGC取得的效果。

GC之痛

很多低延遲高可用Java服務的系統可用性經常受GC停頓的困擾。GC停頓指垃圾回收期間STW(Stop The World),當STW時,所有應用線程停止活動,等待GC停頓結束。以美團風控服務為例,部分上遊業務要求風控服務65ms内傳回結果,并且可用性要達到99.99%。但因為GC停頓,我們未能達到上述可用性目标。當時使用的是CMS垃圾回收器,單次Young GC 40ms,一分鐘10次,接口平均響應時間30ms。通過計算可知,有(40ms + 30ms) * 10次 / 60000ms = 1.12%的請求的響應時間會增加0 ~ 40ms不等,其中30ms * 10次 / 60000ms = 0.5%的請求響應時間會增加40ms。可見,GC停頓對響應時間的影響較大。為了降低GC停頓對系統可用性的影響,我們從降低單次GC時間和降低GC頻率兩個角度出發進行了調優,還測試過G1垃圾回收器,但這三項措施均未能降低GC對服務可用性的影響。

CMS與G1停頓時間瓶頸

在介紹ZGC之前,首先回顧一下CMS和G1的GC過程以及停頓時間的瓶頸。CMS新生代的Young GC、G1和ZGC都基于标記-複制算法,但算法具體實作的不同就導緻了巨大的性能差異。

标記-複制算法應用在CMS新生代(ParNew是CMS預設的新生代垃圾回收器)和G1垃圾回收器中。标記-複制算法可以分為三個階段:

  • 标記階段,即從GC Roots集合開始,标記活躍對象;
  • 轉移階段,即把活躍對象複制到新的記憶體位址上;
  • 重定位階段,因為轉移導緻對象的位址發生了變化,在重定位階段,所有指向對象舊位址的指針都要調整到對象新的位址上。

下面以G1為例,通過G1中标記-複制算法過程(G1的Young GC和Mixed GC均采用該算法),分析G1停頓耗時的主要瓶頸。G1垃圾回收周期如下圖所示:

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

G1的混合回收過程可以分為标記階段、清理階段和複制階段。

标記階段停頓分析

  • 初始标記階段:初始标記階段是指從GC Roots出發标記全部直接子節點的過程,該階段是STW的。由于GC Roots數量不多,通常該階段耗時非常短。
  • 并發标記階段:并發标記階段是指從GC Roots開始對堆中對象進行可達性分析,找出存活對象。該階段是并發的,即應用線程和GC線程可以同時活動。并發标記耗時相對長很多,但因為不是STW,是以我們不太關心該階段耗時的長短。
  • 再标記階段:重新标記那些在并發标記階段發生變化的對象。該階段是STW的。

清理階段停頓分析

  • 清理階段清點出有存活對象的分區和沒有存活對象的分區,該階段不會清理垃圾對象,也不會執行存活對象的複制。該階段是STW的。

複制階段停頓分析

  • 複制算法中的轉移階段需要配置設定新記憶體和複制對象的成員變量。轉移階段是STW的,其中記憶體配置設定通常耗時非常短,但對象成員變量的複制耗時有可能較長,這是因為複制耗時與存活對象數量與對象複雜度成正比。對象越複雜,複制耗時越長。

四個STW過程中,初始标記因為隻标記GC Roots,耗時較短。再标記因為對象數少,耗時也較短。清理階段因為記憶體分區數量少,耗時也較短。轉移階段要處理所有存活的對象,耗時會較長。是以,G1停頓時間的瓶頸主要是标記-複制中的轉移階段STW。為什麼轉移階段不能和标記階段一樣并發執行呢?主要是G1未能解決轉移過程中準确定位對象位址的問題。

G1的Young GC和CMS的Young GC,其标記-複制全過程STW,這裡不再詳細闡述。

ZGC原理

全并發的ZGC

與CMS中的ParNew和G1類似,ZGC也采用标記-複制算法,不過ZGC對該算法做了重大改進:ZGC在标記、轉移和重定位階段幾乎都是并發的,這是ZGC實作停頓時間小于10ms目标的最關鍵原因。

ZGC垃圾回收周期如下圖所示:

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

ZGC隻有三個STW階段:初始标記,再标記,初始轉移。其中,初始标記和初始轉移分别都隻需要掃描所有GC Roots,其處理時間和GC Roots的數量成正比,一般情況耗時非常短;再标記階段STW時間很短,最多1ms,超過1ms則再次進入并發标記階段。即,ZGC幾乎所有暫停都隻依賴于GC Roots集合大小,停頓時間不會随着堆的大小或者活躍對象的大小而增加。與ZGC對比,G1的轉移階段完全STW的,且停頓時間随存活對象的大小增加而增加。

ZGC關鍵技術

ZGC通過着色指針和讀屏障技術,解決了轉移過程中準确通路對象的問題,實作了并發轉移。大緻原理描述如下:并發轉移中“并發”意味着GC線程在轉移對象的過程中,應用線程也在不停地通路對象。假設對象發生轉移,但對象位址未及時更新,那麼應用線程可能通路到舊位址,進而造成錯誤。而在ZGC中,應用線程通路對象将觸發“讀屏障”,如果發現對象被移動了,那麼“讀屏障”會把讀出來的指針更新到對象的新位址上,這樣應用線程始終通路的都是對象的新位址。那麼,JVM是如何判斷對象被移動過呢?就是利用對象引用的位址,即着色指針。下面介紹着色指針和讀屏障技術細節。

着色指針

着色指針是一種将資訊存儲在指針中的技術。

ZGC僅支援64位系統,它把64位虛拟位址空間劃分為多個子空間,如下圖所示:

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

其中,[0~4TB) 對應Java堆,[4TB ~ 8TB) 稱為M0位址空間,[8TB ~ 12TB) 稱為M1位址空間,[12TB ~ 16TB) 預留未使用,[16TB ~ 20TB) 稱為Remapped空間。

當應用程式建立對象時,首先在堆空間申請一個虛拟位址,但該虛拟位址并不會映射到真正的實體位址。ZGC同時會為該對象在M0、M1和Remapped位址空間分别申請一個虛拟位址,且這三個虛拟位址對應同一個實體位址,但這三個空間在同一時間有且隻有一個空間有效。ZGC之是以設定三個虛拟位址空間,是因為它使用“空間換時間”思想,去降低GC停頓時間。“空間換時間”中的空間是虛拟空間,而不是真正的實體空間。後續章節将詳細介紹這三個空間的切換過程。

與上述位址空間劃分相對應,ZGC實際僅使用64位位址空間的第0~41位,而第42~45位存儲中繼資料,第47~63位固定為0。

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

ZGC将對象存活資訊存儲在42~45位中,這與傳統的垃圾回收并将對象存活資訊放在對象頭中完全不同。

讀屏障

讀屏障是JVM向應用代碼插入一小段代碼的技術。當應用線程從堆中讀取對象引用時,就會執行這段代碼。需要注意的是,僅“從堆中讀取對象引用”才會觸發這段代碼。

讀屏障示例:

Object o = obj.FieldA   // 從堆中讀取引用,需要加入屏障
<Load barrier>
Object p = o  // 無需加入屏障,因為不是從堆中讀取引用
o.dosomething() // 無需加入屏障,因為不是從堆中讀取引用
int i =  obj.FieldB  //無需加入屏障,因為不是對象引用
           

ZGC中讀屏障的代碼作用:在對象标記和轉移過程中,用于确定對象的引用位址是否滿足條件,并作出相應動作。

ZGC并發處理示範

接下來詳細介紹ZGC一次垃圾回收周期中位址視圖的切換過程:

  • 初始化:ZGC初始化之後,整個記憶體空間的位址視圖被設定為Remapped。程式正常運作,在記憶體中配置設定對象,滿足一定條件後垃圾回收啟動,此時進入标記階段。
  • 并發标記階段:第一次進入标記階段時視圖為M0,如果對象被GC标記線程或者應用線程通路過,那麼就将對象的位址視圖從Remapped調整為M0。是以,在标記階段結束之後,對象的位址要麼是M0視圖,要麼是Remapped。如果對象的位址是M0視圖,那麼說明對象是活躍的;如果對象的位址是Remapped視圖,說明對象是不活躍的。
  • 并發轉移階段:标記結束後就進入轉移階段,此時位址視圖再次被設定為Remapped。如果對象被GC轉移線程或者應用線程通路過,那麼就将對象的位址視圖從M0調整為Remapped。

其實,在标記階段存在兩個位址視圖M0和M1,上面的過程顯示隻用了一個位址視圖。之是以設計成兩個,是為了差別前一次标記和目前标記。也即,第二次進入并發标記階段後,位址視圖調整為M1,而非M0。

着色指針和讀屏障技術不僅應用在并發轉移階段,還應用在并發标記階段:将對象設定為已标記,傳統的垃圾回收器需要進行一次記憶體通路,并将對象存活資訊放在對象頭中;而在ZGC中,隻需要設定指針位址的第42~45位即可,并且因為是寄存器通路,是以速度比通路記憶體更快。

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

ZGC調優實踐

ZGC不是“銀彈”,需要根據服務的具體特點進行調優。網絡上能搜尋到實戰經驗較少,調優理論需自行摸索,我們在此階段也耗費了不少時間,最終才達到理想的性能。本文的一個目的是列舉一些使用ZGC時常見的問題,幫助大家使用ZGC提高服務可用性。

調優基礎知識

了解ZGC重要配置參數

以我們服務在生産環境中ZGC參數配置為例,說明各個參數的作用:

重要參數配置樣例:

-Xms10G -Xmx10G 
-XX:ReservedCodeCacheSize=256m -XX:InitialCodeCacheSize=256m 
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC 
-XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 
-XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive 
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m 
           

-Xms -Xmx:堆的最大記憶體和最小記憶體,這裡都設定為10G,程式的堆記憶體将保持10G不變。 -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize:設定CodeCache的大小, JIT編譯的代碼都放在CodeCache中,一般服務64m或128m就已經足夠。我們的服務因為有一定特殊性,是以設定的較大,後面會詳細介紹。 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC:啟用ZGC的配置。 -XX:ConcGCThreads:并發回收垃圾的線程。預設是總核數的12.5%,8核CPU預設是1。調大後GC變快,但會占用程式運作時的CPU資源,吞吐會受到影響。 -XX:ParallelGCThreads:STW階段使用線程數,預設是總核數的60%。 -XX:ZCollectionInterval:ZGC發生的最小時間間隔,機關秒。 -XX:ZAllocationSpikeTolerance:ZGC觸發自适應算法的修正系數,預設2,數值越大,越早的觸發ZGC。 -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否啟用主動回收,預設開啟,這裡的配置表示關閉。 -Xlog:設定GC日志中的内容、格式、位置以及每個日志的大小。

了解ZGC觸發時機

相比于CMS和G1的GC觸發機制,ZGC的GC觸發機制有很大不同。ZGC的核心特點是并發,GC過程中一直有新的對象産生。如何保證在GC完成之前,新産生的對象不會将堆占滿,是ZGC參數調優的第一大目标。因為在ZGC中,當垃圾來不及回收将堆占滿時,會導緻正在運作的線程停頓,持續時間可能長達秒級之久。

ZGC有多種GC觸發機制,總結如下:

  • 阻塞記憶體配置設定請求觸發:當垃圾來不及回收,垃圾将堆占滿時,會導緻部分線程阻塞。我們應當避免出現這種觸發方式。日志中關鍵字是“Allocation Stall”。
  • 基于配置設定速率的自适應算法:最主要的GC觸發方式,其算法原理可簡單描述為”ZGC根據近期的對象配置設定速率以及GC時間,計算出當記憶體占用達到什麼門檻值時觸發下一次GC”。自适應算法的詳細理論可參考彭成寒《新一代垃圾回收器ZGC設計與實作》一書中的内容。通過ZAllocationSpikeTolerance參數控制門檻值大小,該參數預設2,數值越大,越早的觸發GC。我們通過調整此參數解決了一些問題。日志中關鍵字是“Allocation Rate”。
  • 基于固定時間間隔:通過ZCollectionInterval控制,适合應對突增流量場景。流量平穩變化時,自适應算法可能在堆使用率達到95%以上才觸發GC。流量突增時,自适應算法觸發的時機可能會過晚,導緻部分線程阻塞。我們通過調整此參數解決流量突增場景的問題,比如定時活動、秒殺等場景。日志中關鍵字是“Timer”。
  • 主動觸發規則:類似于固定間隔規則,但時間間隔不固定,是ZGC自行算出來的時機,我們的服務因為已經加了基于固定時間間隔的觸發機制,是以通過-ZProactive參數将該功能關閉,以免GC頻繁,影響服務可用性。 日志中關鍵字是“Proactive”。
  • 預熱規則:服務剛啟動時出現,一般不需要關注。日志中關鍵字是“Warmup”。
  • 外部觸發:代碼中顯式調用System.gc()觸發。 日志中關鍵字是“System.gc()”。
  • 中繼資料配置設定觸發:中繼資料區不足時導緻,一般不需要關注。 日志中關鍵字是“Metadata GC Threshold”。

了解ZGC日志

一次完整的GC過程,需要注意的點已在圖中标出。

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

注意:該日志過濾了進入安全點的資訊。正常情況,在一次GC過程中還穿插着進入安全點的操作。

GC日志中每一行都注明了GC過程中的資訊,關鍵資訊如下:

  • Start:開始GC,并标明的GC觸發的原因。上圖中觸發原因是自适應算法。
  • Phase-Pause Mark Start:初始标記,會STW。
  • Phase-Pause Mark End:再次标記,會STW。
  • Phase-Pause Relocate Start:初始轉移,會STW。
  • Heap資訊:記錄了GC過程中Mark、Relocate前後的堆大小變化狀況。High和Low記錄了其中的最大值和最小值,我們一般關注High中Used的值,如果達到100%,在GC過程中一定存在記憶體配置設定不足的情況,需要調整GC的觸發時機,更早或者更快地進行GC。
  • GC資訊統計:可以定時的列印垃圾收集資訊,觀察10秒内、10分鐘内、10個小時内,從啟動到現在的所有統計資訊。利用這些統計資訊,可以排查定位一些異常點。

日志中内容較多,關鍵點已用紅線标出,含義較好了解,更詳細的解釋大家可以自行在網上查閱資料。

【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

了解ZGC停頓原因

我們在實戰過程中共發現了6種使程式停頓的場景,分别如下:

  • GC時,初始标記:日志中Pause Mark Start。
  • GC時,再标記:日志中Pause Mark End。
  • GC時,初始轉移:日志中Pause Relocate Start。
  • 記憶體配置設定阻塞:當記憶體不足時線程會阻塞等待GC完成,關鍵字是”Allocation Stall”。
【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐
  • 安全點:所有線程進入到安全點後才能進行GC,ZGC定期進入安全點判斷是否需要GC。先進入安全點的線程需要等待後進入安全點的線程直到所有線程挂起。
  • dump線程、記憶體:比如jstack、jmap指令。
【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐
【美團技術團隊搬運】新一代垃圾回收器ZGC的探索與實踐新一代垃圾回收器ZGC的探索與實踐

調優案例

我們維護的服務名叫Zeus,它是美團的規則平台,常用于風控場景中的規則管理。規則運作是基于開源的表達式執行引擎Aviator。Aviator内部将每一條表達式轉化成Java的一個類,通過調用該類的接口實作表達式邏輯。

Zeus服務内的規則數量超過萬條,且每台機器每天的請求量幾百萬。這些客觀條件導緻Aviator生成的類和方法會産生很多的ClassLoader和CodeCache,這些在使用ZGC時都成為過GC的性能瓶頸。接下來介紹兩類調優案例。

記憶體配置設定阻塞,系統停頓可達到秒級

案例一:秒殺活動中流量突增,出現性能毛刺

日志資訊:對比出現性能毛刺時間點的GC日志和業務日志,發現JVM停頓了較長時間,且停頓時GC日志中有大量的“Allocation Stall”日志。

分析:這種案例多出現在“自适應算法”為主要GC觸發機制的場景中。ZGC是一款并發的垃圾回收器,GC線程和應用線程同時活動,在GC過程中,還會産生新的對象。GC完成之前,新産生的對象将堆占滿,那麼應用線程可能因為申請記憶體失敗而導緻線程阻塞。當秒殺活動開始,大量請求打入系統,但自适應算法計算的GC觸發間隔較長,導緻GC觸發不及時,引起了記憶體配置設定阻塞,導緻停頓。

解決方法:

(1)開啟”基于固定時間間隔“的GC觸發機制:-XX:ZCollectionInterval。比如調整為5秒,甚至更短。

(2)增大修正系數-XX:ZAllocationSpikeTolerance,更早觸發GC。ZGC采用正态分布模型預測記憶體配置設定速率,模型修正系數ZAllocationSpikeTolerance預設值為2,值越大,越早的觸發GC,Zeus中所有叢集設定的是5。

案例二:壓測時,流量逐漸增大到一定程度後,出現性能毛刺

日志資訊:平均1秒GC一次,兩次GC之間幾乎沒有間隔。

分析:GC觸發及時,但記憶體标記和回收速度過慢,引起記憶體配置設定阻塞,導緻停頓。

解決方法:增大-XX:ConcGCThreads, 加快并發标記和回收速度。ConcGCThreads預設值是核數的1/8,8核機器,預設值是1。該參數影響系統吞吐,如果GC間隔時間大于GC周期,不建議調整該參數。

GC Roots 數量大,單次GC停頓時間長

案例三: 單次GC停頓時間30ms,與預期停頓10ms左右有較大差距

日志資訊:觀察ZGC日志資訊統計,“Pause Roots ClassLoaderDataGraph”一項耗時較長。

分析:dump記憶體檔案,發現系統中有上萬個ClassLoader執行個體。我們知道ClassLoader屬于GC Roots一部分,且ZGC停頓時間與GC Roots成正比,GC Roots數量越大,停頓時間越久。再進一步分析,ClassLoader的類名表明,這些ClassLoader均由Aviator元件生成。分析Aviator源碼,發現Aviator對每一個表達式新生成類時,會建立一個ClassLoader,這導緻了ClassLoader數量巨大的問題。在更高Aviator版本中,該問題已經被修複,即僅建立一個ClassLoader為所有表達式生成類。

解決方法:更新Aviator元件版本,避免生成多餘的ClassLoader。

案例四:服務啟動後,運作時間越長,單次GC時間越長,重新開機後恢複

日志資訊:觀察ZGC日志資訊統計,“Pause Roots CodeCache”的耗時會随着服務運作時間逐漸增長。

分析:CodeCache空間用于存放Java熱點代碼的JIT編譯結果,而CodeCache也屬于GC Roots一部分。通過添加-XX:+PrintCodeCacheOnCompilation參數,列印CodeCache中的被優化的方法,發現大量的Aviator表達式代碼。定位到根本原因,每個表達式都是一個類中一個方法。随着運作時間越長,執行次數增加,這些方法會被JIT優化編譯進入到Code Cache中,導緻CodeCache越來越大。

解決方法:JIT有一些參數配置可以調整JIT編譯的條件,但對于我們的問題都不太适用。我們最終通過業務優化解決,删除不需要執行的Aviator表達式,進而避免了大量Aviator方法進入CodeCache中。

值得一提的是,我們并不是在所有這些問題都解決後才全量部署所有叢集。即使開始有各種各樣的毛刺,但計算後發現,有各種問題的ZGC也比之前的CMS對服務可用性影響小。是以從開始準備使用ZGC到全量部署,大概用了2周的時間。在之後的3個月時間裡,我們邊做業務需求,邊跟進這些問題,最終逐個解決了上述問題,進而使ZGC在各個叢集上達到了一個更好表現。

更新ZGC效果

延遲降低

TP(Top Percentile)是一項衡量系統延遲的名額:TP999表示99.9%請求都能被響應的最小耗時;TP99表示99%請求都能被響應的最小耗時。

在Zeus服務不同叢集中,ZGC在低延遲(TP999 < 200ms)場景中收益較大:

  • TP999:下降12~142ms,下降幅度18%~74%。
  • TP99:下降5~28ms,下降幅度10%~47%。

超低延遲(TP999 < 20ms)和高延遲(TP999 > 200ms)服務收益不大,原因是這些服務的響應時間瓶頸不是GC,而是外部依賴的性能。

吞吐下降

對吞吐量優先的場景,ZGC可能并不适合。例如,Zeus某離線叢集原先使用CMS,更新ZGC後,系統吞吐量明顯降低。究其原因有二:第一,ZGC是單代垃圾回收器,而CMS是分代垃圾回收器。單代垃圾回收器每次處理的對象更多,更耗費CPU資源;第二,ZGC使用讀屏障,讀屏障操作需耗費額外的計算資源。

總結

ZGC作為下一代垃圾回收器,性能非常優秀。ZGC垃圾回收過程幾乎全部是并發,實際STW停頓時間極短,不到10ms。這得益于其采用的着色指針和讀屏障技術。

Zeus在更新JDK 11+ZGC中,通過将風險和問題分類,然後各個擊破,最終順利實作了更新目标,GC停頓也幾乎不再影響系統可用性。

最後推薦大家更新ZGC,Zeus系統因為業務特點,遇到了較多問題,而風控其他團隊在更新時都非常順利。歡迎大家加入“ZGC使用交流”群。

參考文獻

  • ZGC官網
  • 彭成寒.《新一代垃圾回收器ZGC設計與實作》. 機械工業出版社, 2019.
  • 從實際案例聊聊Java應用的GC優化
  • Java Hotspot G1 GC的一些關鍵技術

附錄

如何使用新技術

在生産環境更新JDK 11,使用ZGC,大家最關心的可能不是效果怎麼樣,而是這個新版本用的人少,網上實踐也少,靠不靠譜,穩不穩定。其次是更新成本會不會很大,萬一不成功豈不是白白浪費時間。是以,在使用新技術前,首先要做的是評估收益、成本和風險。

評估收益

對于JDK這種世界關注的程式,大版本更新所引入的新技術一般已經在理論上經過驗證。我們要做的事情就是确定目前系統的瓶頸是否是新版本JDK可解決的問題,切忌問題未診斷清楚就采取措施。評估完收益之後再評估成本和風險,收益過大或者過小,其他兩項影響權重就會小很多。

以本文開頭提到的案例為例,假設GC次數不變(10次/分鐘),且單次GC時間從40ms降低10ms。通過計算,一分鐘内有100/60000 = 0.17%的時間在進行GC,且期間所有請求僅停頓10ms,GC期間影響的請求數和因GC增加的延遲都有所減少。

評估成本

這裡主要指更新所需要的人力成本。此項相對比較成熟,根據新技術的使用手冊判斷改動點。跟做其他項目差別不大,不再具體細說。

在我們的實踐中,兩周時間完成線上部署,達到安全穩定運作的狀态。後續持續疊代3個月,根據業務場景對ZGC進行了更契合的優化适配。

評估風險

更新JDK的風險可以分為三類:

  • 相容性風險:Java程式JAR包依賴很多,更新JDK版本後程式是否能運作起來。例如我們的服務是從JDK 7更新到JDK 11,需要解決較多JAR包不相容的問題。
  • 功能風險:運作起來後,是否會有一些元件邏輯變更,影響現有功能的邏輯。
  • 性能風險:功能如果沒有問題,性能是否穩定,能穩定的線上上運作。

經過分類後,每類風險的應對轉化成了常見的測試問題,不再屬于未知風險。風險是指不确定的事情,如果不确定的事情都能轉化成可确定的事情,意味着風險已消除。

更新JDK 11

選擇JDK 11,是因為在JDK 11中首次支援ZGC,而且JDK 11屬于長期支援(Long Term Support,LTS)版本,至少會被維護三年,普通版本(如JDK 12、JDK 13和JDK 14)隻有6個月的維護周期,不建議使用。

本地測試環境安裝

從兩個源OpenJDK和OracleJDK 下載下傳JDK 11,二個版本的JDK主要差別是長時期的免費和付費,短期内都免費。注意JDK 11版本中的ZGC不支援Mac OS系統,在Mac OS系統上使用JDK 11隻能用其他垃圾回收器,如G1。

生産環境安裝

更新JDK 11不僅僅是更新自己項目的JDK版本,還需要編譯、釋出部署、運作、監控、性能記憶體分析工具等項目支援。美團内部的實踐:

編譯打包:美團釋出系統支援選擇JDK 11進行編譯打包。 線上運作 & 全量部署:要求線上機器已安裝JDK11,有3種方式:

1.新申請預設安裝JDK 11的虛拟機:試用JDK 11時可用這種方式;全量部署時,如果新申請機器數量過多,可能沒有足夠機器資源。 2.通過手寫腳本給存量虛拟機安裝JDK 11:不推薦,業務同學過多參與到運維當中。 3.使用容器提供的鏡像部署功能,在打包鏡像時安裝JDK 11:推薦方式,不需要新申請資源。

監控名額:主要是GC的時間和頻率,我們通過美團的CAT監控系統支援ZGC資料的收集(CAT已開源)。 性能記憶體分析:線上遇到性能問題時,還需要借助Profiling工具,美團的性能診斷優化平台Scalpel已支援JDK 11的性能記憶體分析。如果你的公司沒有相關工具,推薦使用JProfier。

解決元件相容性

我們的項目包含二十多萬行代碼,需要從JDK 7更新到JDK 11,依賴元件衆多。雖然看起來更新會比較複雜,但實際隻花了兩天時間即解決了相容性問題。具體過程如下:

1.編譯,需要修改pom檔案中的build配置,根據報錯作修改,主要有兩類:

a.一些類被删除:比如“sun.misc.BASE64Encoder”,找到替換類java.util.Base64即可。

b.元件依賴版本不相容JDK 11問題:找到對應依賴元件,搜尋最新版本,一般都支援JDK 11。

2.編譯成功後,啟動運作,此時仍有可能元件依賴版本問題,按照編譯時的方式處理即可。

更新所修改的依賴:

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.4</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-parent</artifactId>
    <version>6.0.16.Final</version>
</dependency>
<dependency>
    <groupId>com.sankuai.inf</groupId>
    <artifactId>patriot-sdk</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.39.Final</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
           

JDK 11已經出來兩年,常見的依賴元件都有相容性版本。但是,如果是公司内部提供的公司級元件,可能會不相容JDK 11,需要推動相關元件進行更新。如果對方更新較為困難,可以考慮拆分功能,将依賴這些元件的功能單獨部署,繼續使用低版本JDK。随着JDK11的卓越性能被大家悉知,相信會有更多團隊會用JDK 11解決GC問題,使用者越多,各個元件更新的動力也會越大。

驗證功能正确性

通過完備的單測、內建和回歸測試,保證功能正确性。

作者簡介

  • 王東,美團資訊安全資深工程師。
  • 王偉,美團資訊安全技術專家。

安全, 基礎研發平台, ZGC, 運維, G1, CMS, GC