天天看點

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

作者:大資料架構師

ZGC新特性概覽

ZGC自2018年9月釋出以後就成為OpenJDK中最為活躍的項目,在每個版本中都有重要的特性釋出。下面簡單梳理一下ZGC從JDK 11到JDK 16的主要特性,如表8-4所示。

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

表8-4 ZGC重要特性釋出概覽

http://openjdk.java.net/jeps/351

http://openjdk.java.net/jeps/364

http://openjdk.java.net/jeps/365

http://openjdk.java.net/jeps/365

http://openjdk.java.net/jeps/376

在這些特性中有兩個特性是針對停頓時間進一步的優化,分别是JDK 12引入的并發類解除安裝和JDK 16引入的并發棧掃描。下面對這兩個特性進一步介紹。

并發類解除安裝

在JVM中類解除安裝很常見,指的是在垃圾回收的過程中将應用不再使用的類及相關中繼資料占用的記憶體進行回收。故類解除安裝也稱為類回收。

并發類解除安裝的核心在于将位于STW中的并行标記優化為并發标記,進而達到減少停頓時間的目的。在垃圾回收的過程中,需要周遊線程棧,如果發現棧變量指向的堆空間中的對象,則說明對象活躍。而在棧掃描的過程中需要處理本地棧。這裡的本地棧指的是Java代碼被JIT編譯成本地代碼,本地代碼在執行時使用的棧。本地棧中同樣存在棧變量指向的堆空間中的對象,是以在初始标記階段會并行地處理本地棧。

在再标記階段,由于線程棧發生了變化(例如新增了線程棧幀),是以需要重新掃描線程棧。

并發類解除安裝指的是将并行标記本地棧的工作盡量并發執行。将本地線程棧幀分為兩種,并對這兩種不同狀态的本地線程棧幀做不同的處理。

1)初始标記階段已經執行的本地線程棧幀:在初始标記階段全部周遊線程中的本地線程棧幀,周遊得到對象作為根集合待并發标記執行。

2)并發階段新增待執行的本地線程棧幀:截獲新進入的本地線程棧幀,當發現線程需要執行新的本地線程棧幀時,線程幫助GC工作線程主動地周遊棧變量,并将其作為标記的根集合。

對于第一種情況,不需要做任何額外的動作。對于第二種情況,ZGC引入了一種新的本地方法——屏障,用于識别新進入的本地線程棧幀,用Java位元組碼方法編譯成本地方法後,在本地方法注冊到JVM中(待後續替換位元組碼方法),會先周遊本地方法中用到的對象(JIT在編譯的時候知道所有對象的資訊,并将這些資訊通過一個資料結構進行儲存),并将這些對象作為根集合對象,周遊其成員變量。這兩種棧幀的标記方式如圖8-24所示。

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

圖8-24 ZGC中兩種棧幀标記示意圖

對于本地方法,還需要考慮弱引用的支援,特别是重寫了finalize函數或者使用了虛引用時。對于這些本地方法,需要在處理完弱引用後才能回收。

本地方法之間通過連結清單關聯,所有的本地方法都可以從連結清單頭對所有的本地方法進行周遊,如圖8-25所示。

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

圖8-25 編譯後本地方法管理連結清單

在所有的本地方法中,如果在标記階段完成了标記,則說明本地方法是強根,所發現的對象都是強引用。是以還需要把本地方法作為弱根繼續周遊,沒有執行過的本地方法,通過GC工作線程進行标記,被視為弱根。但是由于Mutator和GC工作線程并發執行,可能存在一些對象,先由GC工作線程進行标記,後被Mutator激活,此時這些對象并不需要重新标記,但是需要将對象變成強根處理。

并發根掃描

并發根掃描是進一步減少停頓時間的一個重要手段。對于已經生産的線程棧幀,在初始标記時需要一次性地全部标記完成。這在一些場景中(例如線程在執行過程中調用深度很深,則線程棧中包含了大量的棧幀)可能會導緻初始标記時間很長,是以需要進一步将線程棧幀并發處理,以保證STW總是在一個很短的時間内完成。

并發處理的一個思路是:在進入初始标記階段,僅僅标記目前正在執行的棧幀,同時設定一個屏障(記錄已經标記的棧幀)。在進入并發标記階段時,如果Mutator沒有通路到尚未标記處理的棧幀(通過屏障可以判斷),則這些棧幀可以由GC工作線程進行标記。如果Mutator通路到這些尚未标記的棧幀,則由Mutator幫助GC工作線程對棧幀進行标記。ZGC使用一個水印(WaterMark)來記錄已經标記的棧幀位址。

雖然思路并不複雜,但實作起來并不容易。其中一個難點在于參數的處理。首先通過一個簡單的例子來了解一下參數的傳遞。假設有兩個方法,分别為caller和callee,其中caller調用callee,并傳遞兩個參數給callee。

在方法調用的過程中,不同的平台對于參數傳遞有不同的約定(主要涉及寄存器的儲存、參數傳遞的方式等)。在X86和AArch64等主流平台上,對于參數通常的處理方法是在caller線程棧中配置設定局部記憶體,并将參數複制到棧中,而在callee中通過棧幀指針的偏移可以通路caller中的參數,如圖8-26所示。

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

圖8-26 調用通路示意圖

由于caller和callee都可以通路參數,如果參數中指向了堆中的對象,僅僅标記目前棧幀(callee),沒有标記caller中的棧幀,此時callee可能指向一個過時的對象,并且後續所有的對象都指向一個過時的引用。這意味着在标記階段結束後,有的對象的狀态不正确。

為此ZGC中是在标記“目前棧幀”,但實際上是把目前棧幀的callee和caller同時進行周遊。另外,JIT中為了執行一些本地方法,通常會生成一個stub函數用于輔助本地方法的執行。是以ZGC在對“目前棧幀”周遊時實際上處理了stub、callee和caller這3個棧幀,同時設定WaterMark用于标記棧幀是否處理,如圖8-27所示。

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

圖8-27 并發棧标記示意圖

并發根掃描技術可有效地降低停頓時間。最新的測試報告顯示,對于Specjbb 2015,停頓時間低至0.2毫秒,這對于許多應用來說是非常有利的。

但是,需要指出ZGC一個最大的缺點:雖然ZGC将全局停頓時間減少到亞秒級,但是它是以降低吞吐量為前提的,Mutator在記憶體不足時,會暫停自己,等待垃圾回收完成後再繼續執行(在日志中會出現StallAllocation)。另外需要說明的是,當一個Mutator出現暫停執行的情況時,通常其他Mutator也會出現類似的情況,這在本質上等價于全局暫停。目前對于這樣的情況,ZGC并沒有很好的解決方法,通常可以通過增加記憶體,或者調整參數讓垃圾回收更早發生或者更高效地回收來解決。

擴充閱讀:配置設定預測模型的理論基礎

ZGC中使用兩個數學模型進行系統中資料的預測,這兩個模型分别是正态分布模型和衰減均值模型。在前面介紹了衰減平均值模型,也介紹了正态分布模型。這兩個模型分布用于什麼場景?

1)正态分布使用曆史資料對未來資料進行預測,是使用最廣的一個模型,它的使用場景是:變量的變化受外部變量的影響,即變量變化除了随着自身系統的運作變化以外,還有外部輸入對其産生影響,此時使用衰減平均值模型不能反映外部輸入變化。是以假定變量符合一定的機率分布,然後通過分布模型來預測。例如ZGC中在考慮記憶體在多長時間被Mutator配置設定完畢時就采用了正态分布進行預測,其主要原因是配置設定時間與Mutator對記憶體使用密切相關。

2)衰減平均值通常用于單一變量的預測,即使用曆史資料來預測未來的資料,同時給不同時間的曆史資料使用不同的權重系數,距離目前時間越近,權重越大,距離目前時間越遠,權重越小。使用衰減均值模型比一般的算術均值模型更為強調最新資料的重要,能較快地反映變量的波動性。使用衰減平均值的情形通常是:變量的變化不受外部變量的影響(或者說影響變量的因素太多),即變量隻随着系統本身的變化而變化,并沒有輸入的影響。例如,在ZGC中通常對于GC的執行時間使用衰減平均值模型,其基本的假設是GC的運作不受外部應用的影響,隻與JVM本身有關。

在使用衰減平均值模型時,需要考慮一個問題:該收集多少曆史資料才可以比較準确地預測未來的資料?

在ZGC的實作中經常看到魔數3,例如預熱規則中要求觸發3次垃圾回收就認為有足夠的資料。為什麼選擇數字3?其理論依據是什麼?

在JVM的實作中用到兩種平均值進行預測,分别是算術平均和權重平均。

下面通過例子簡單地介紹相關概念。

假設有n個數,記為X1, X2, …, Xn。則n個數的算術平均值為

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

如果每個數的權重不同,權重記為W1, W2, …, Wn,則n個數的算術權重平均

值為

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

,其中

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

利用均值做預測的優勢是計算量少,如果輸入序列随着時間不斷地增加,則均值能較好地反映輸入序列的趨勢。

在實際場景中,最新的輸入值更能反映系統最近的狀态,是以攜帶了更多的資訊。那麼從均值角度來看,給最新的輸入值以更大的權重計算得到的均值更符合實際情況。

均值法的基本原理如下:通過移動平均值消除時間序列中不規則的變動和其他變化,進而揭示時間序列的長期趨勢。

如何優化計算和存儲呢?

對于算術平均值可以簡單地記錄總的值和總的次數,每當有新的輸入值的,執行:sum += new,count++,Sum和count,則有Avg = sum /count。早期使用的就是這樣的方式,如CMS中的預測計算公式。

然而效果更好的權重均值則無法通過上述方法進行優化,因為每次計算時每個輸入值的權重不同,這就要求把所有的輸入值都儲存,進而産生了很大的存儲消耗,是以需要進一步對權重進行變化和優化。公式如下:

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

其中Xt為最新的輸入,α是Xt權重。

在計算均值的過程中,越早的輸入權重越低。權重是指數變化趨勢,是以這個公式稱為數值權重平均。

簡單的總結就是:隻需要知道本次的真實輸入值Xt及預測值Ft來預測下一次值Ft+1。這個公式使用起來稍有不便,對其做一個簡單的變換,使用本次的輸入值和上一次預測值來預測目前的值:

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

實體意義:系數α越大,說明過往預測值的權重越低,目前輸入值的權重越高,計算得到的均值時效性越強;反之,系數α越小,計算得到的均值時效性就越弱。指數權重平均還有一個特點,就是具有吸收瞬時突發的能力,進而得到更加平滑的曲線趨勢。當然,系數α越小,曲線的平滑越好,α越大,曲線的平滑越差。

如何計算系數α及确定存儲的長度?推導過程如下:

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

對于公式來說,如果某一項的值趨近于0,則可以忽略。由于1-α小于1,是以(1-α)n-1會越來越小,最終趨近于0。已知

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

,可以得到

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛
程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

,約等于0.35。如果取系數α為

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

,則可以得到(1-α)n的值為

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

,即0.35,最後一項為

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

,隻要

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

足夠小,那麼最後一項可以忽略。

假設

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

足夠小,那麼在不同的系數α的設定下就可以計算得到n的大小。例如,α取0.5,則n隻需要取2就可以;如果α取0.1,則n隻需要取10就可以;如果α取0.02,則n隻需要取98就可以。在JVM中α取0.3,是以隻需要3個資料就可以了。

另外一個問題,當有多少個初始資料以後就可以啟動進行預測?如果前序資料太少,會導緻整個預測失真。是以對上述公式進行修正,修正的方式也很簡單,為每一個預測值增加一個系數。

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

等價于

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

由于αt随着t的變大而變小,當t足夠大時,αt趨近于0。當資料足夠多時,

程式員必知必會的JVM垃圾回收器ZGC的新特性,了解透了面試不虛

和Vt一緻。同樣的道理,隻要任務αt趨近于0,就可以啟動預測。α取0.3,是以隻需要經過3次預熱就能保證預測的啟動誤差比較小。是以在ZGC實作中,隻要經過3次預熱就可以使用公式進行預測的理論基礎就源于此。

本篇文章給大家講解的内容是JVM垃圾回收器詳解:ZGC新特性概覽

  1. 下篇文章給大家講解的内容是JVM中垃圾回收相關參數介紹:GC通用參數,GC生産參數
  2. 感謝大家的支援!

繼續閱讀