垃圾回收實作-垃圾回收政策
Shenandoah為了滿足不同的使用場景,在垃圾回收時設計了4種不同的垃圾回收政策,分别是static、aggressive、adaptive和compact。每種政策觸發垃圾回收的條件略有不同。
不同的回收政策除了控制如何啟動垃圾回收之外,還會控制記憶體中的哪些記憶體可以被回收。這4種政策對應的回收觸發條件和回收範圍總結如表7-2所示。
表7-2 Shenandoah垃圾回收政策
垃圾回收的政策可以通過參數控制,預設的政策是adaptive。另外,這4種模式回收的分區在不同的版本中可能略有差別,但總體來說差别不大。
垃圾回收模式
在JDK 12中隻有一種回收模式,但是存在6種回收政策,其中traversal是一種比較特殊的政策。本質上traversal并不是一種回收政策,而是一種回收模式。回收政策定義在回收模式時垃圾回收的粒度,回收模式定義垃圾回收的整個流程。是以traversal政策實際上定義了一種回收模式。但是traversal相關代碼複雜度太高,存在不少問題,是以JDK 15将該模式相關代碼移除。但同時Shenandoah又引入了一種新的模式,稱為增量更新。
在最新的JDK 17中Shenandoah支援3種回收模式:
1)SATB或者Normal模式,在JDK 16之前,名字使用Normal,在JDK 16中名字使用SATB。該模式表示在并發标記時使用SATB的标記算法,可以使用除了Passive政策以外的4種回收政策。
2)Incremental-Update(IU),該模式是在traversal移除後新增的回收模式。該模式指的是在并發标記時使用增量回收的标記算法,可以使用除了Passive政策以外的4種回收政策。
3)Passive模式,該模式僅僅使用Passive回收政策。由于Passive政策僅僅在執行OOM時才會觸發垃圾回收,是以Passive模式在執行垃圾回收時是暫停執行的。
其中SATB模式是成熟的模式,IU模式是實驗模式,Passive模式幾乎不使用。
SATB模式和IU模式最大的差別是通過屏障技術解決并發标記正确性問題的方式不同,SATB模式通過屏障記錄修改前的對象,而IU模式通過屏障記錄引用者。
除了上述3種回收模式以外,本文也稍微提一下已經移除的traversal模式,該模式是一種非常激進的回收方式。
正常回收算法
在JDK 15之前,Shenandoah中有兩種正常回收模式:一般模式和優化模式。
一般模式和優化模式的差別在于是否在标記的時候執行重定位,如果在标記的過程中執行重定位,則稱為優化模式,否則稱為一般模式。
這兩種模式可以通過參數ShenandoahUpdateRefs-Early控制,取值為off/false表示垃圾回收執行優化模式,on/true/adaptive表示執行一般模式。
一般模式垃圾回收的步驟如下:
1)初始标記:從根集合出發,标記根集合所有引用的對象,這些對象作為下一步并發标記的出發點。這一步是在STW中進行的。
2)并發标記:以第一步标記的對象作為出發點,開始并發地标記對象。
3)預清理:在進入再标記階段之前,先處理引用對象,把仍然活躍的引用對象重新激活,不進行真正的垃圾回收。該階段支援并發執行,但是隻有一個并發工作線程執行預清理。
4)再标記:該階段要做3件事情,分别為終止标記、計算回收集、轉移根集合直接的引用對象。這一步是在STW中進行的。
5)清理:再标記結束後,部分分區可能已經沒有任何活躍對象,這些分區就可以被回收了。
6)并發轉移:根據轉移集,對所有在轉移集中的活躍對象進行轉移。
7)初始重定位:初始重定位将根據标記過程中識别的活躍對象更新分區中對象的記憶體位址。這一步是在STW中進行的。
8)并發重定位:周遊不屬于回收集合中的分區的對象,根據BrookPointer更新對象的引用指針。
9)結束重定位:周遊根集合中所有引用的對象,更新對象的引用指針。
這一步是在STW中進行的。
10)再次清理:因為回收集合中對象全部轉移完成,是以可以釋放空間。
整個垃圾回收的活動如圖7-14所示。
圖7-14 整個垃圾回收活動示意圖
優化模式垃圾回收
優化模式與一般模式非常類似,唯一的差別在于是否合并标記和引用更新。如果合并這兩個階段,則稱為優化模式。優化模式可以減少一次堆周遊,但是在Shenandoah中的優化模式把記憶體釋放一直推遲到下一個垃圾回收周期,這将導緻本應該快速釋放的記憶體無法釋放。在比較優化模式的成本與收益後,在JDK 15中正式将該模式移除。優化回收的步驟如下。
1)初始标記:和一般模式中的初始标記相同。
2)并發标記:以第一步标記的對象作為出發點,開始并發地标記對象。注意在這一步首先判斷對象是否需要重定位,如果需要則進行重定位。
3)預清理:和一般模式中的預清理相同。
4)再标記:該階段主要做4件事情,分别為更新根集合中所有對象的引用、終止标記、計算回收集、轉移根集合直接的引用對象。這一步是在STW中進行的。
5)清理:和一般模式中的清理相同。
6)并發轉移:和一般模式中的并發轉移相同。
7)結束轉移:設定轉移結束标記,重置TLAB等資訊。這一步是在STW中進行的。
垃圾回收的降級
降級回收算法(也稱為Degenerated GC)指在垃圾回收過程中,如果遇到記憶體配置設定失敗,就進入降級回收。降級回收實質上是在STW中并行執行的。
在正常回收運作的過程中,應用程式和垃圾回收線程都可能需要配置設定記憶體空間,也都有可能遇到記憶體不足導緻配置設定失敗的情況,此時正常回收将進入降級回收。如果在降級回收時再遇到記憶體不足的情況,将進入Full GC。這3種算法互動的流程如圖7-15所示。
圖7-15 正常回收、降級回收和Full GC互動的流程
降級回收的步驟和正常回收基本一緻,隻不過降級回收是并行執行的。
降級回收中若再次遇到配置設定失敗,将被進一步降級為Full GC(在并發回收中的配置設定失敗通常是應用請求記憶體配置設定導緻的,而降級回收中的配置設定失敗是GC工作線程請求記憶體配置設定導緻的)。Full GC采用典型的并行标記壓縮回收,和G1的實作非常類似,這裡不再贅述。
周遊回收算法
在介紹垃圾回收時,可以把重定位階段和标記階段進行合并,這個思路就是Shenandoah的優化回收。那麼還能不能再進一步優化這個算法,把并發标記、并發轉移和并發重定位合并放在一個并發步驟中?Shenandoah中的周遊回收實作就是把這3個并發階段合并到一個階段,如圖7-16所示。
從圖7-16中可以看出,在周遊回收中,第一次垃圾回收啟動時進行并發标記,第二次垃圾回收啟動時進行并發标記和并發轉移,第三次垃圾回收和以後的垃圾回收啟動時都可以執行并發标記、并發轉移和并發重定位。
圖7-16 周遊回收示意圖
由于代碼的複雜性,周遊回收在JDK 15中被移除。
垃圾回收觸發的時機
Shenandoah中垃圾回收觸發的時機與垃圾回收的模式和政策密切相關,在後面介紹相關參數時會詳細介紹。例如,Adaptive政策有6種觸發垃圾回收的條件。
其他細節
Shenandoah的實作還有很多細節值得仔細推敲,限于篇幅,這裡隻是稍微介紹讀者容易忽略的兩個細節。
(1)并發轉移是否可以利用SATB相關資訊優化
在并發标記中使用了SATB引入的TAMS指針,分區中該指針以後的對象都是并發标記啟動以後新配置設定的對象。
并發轉移階段中的分區分為兩種:分區中的對象将要被轉移,分區被回收,這些分區位于CSet中;分區不參與回收。CSet中的分區将不會再用于配置設定對象,非CSet中的分區可以繼續用于配置設定對象。對于非CSet的分區可以利用TAMS指針,在并發轉移啟動以後,TAMS指針以後新配置設定的對象狀态都是正确的,新配置設定的對象如果指向尚未完成轉移的對象,就會通過讀屏障将尚未轉移的對象轉移到新的位置,是以TAMS指針以後配置設定的對象都不需要再次更新對象的引用。是以在并發轉移中也使用TAMS指針區分新配置設定對象和尚未完成更新的對象,這将提高并發更新引用的效率。TAMS指針在并發轉移中的使用如圖7-17所示。
圖7-17 TAMS指針在并發轉移中的使用
(2)并發轉移中出現轉移失敗該如何處理
在G1的垃圾回收過程中會申請記憶體用于轉移對象,當無法申請到記憶體時就會導緻對象無法轉移,此時稱為轉移失敗。當轉移失敗後,需要對轉移失敗的對象進行特殊處理,通常是将轉移對象的轉移指針指向自己,避免該對象再次被轉移,同時并不中斷垃圾回收的過程。在垃圾回收結束後,對轉移失敗的情況重新設定對象頭,并更新引用集等資訊。
G1的轉移是并行處理,整個處理不會出現對象狀态不一緻的情況。而Shenandoah是并發轉移,當出現轉移失敗時,需要額外處理,否則将出現對象不一緻的情況。用一個簡單的例子來示範Shenandoah并發轉移可能存在的問題。假設垃圾回收處于并發轉移階段,有兩個線程T1和T2可以通路同一個對象,運作時資訊如圖7-18所示。
圖7-18 并發轉移階段兩個線程通路同一對象
當線程T1或者T2通路對象時,都會先轉移對象到目标空間。假設T1在轉移對象時遇到無法配置設定記憶體的情況,對T1來說就發生了轉移失敗,T1會嘗試标記對象轉移失敗(假設也使用轉移指針指向自己)。同時,線程T2通路對象時也會轉移對象,假設T2有充足的記憶體(例如T2的TLAB中有空閑空間)可以成功轉移記憶體。此時運作時資訊如圖7-19所示。
圖7-19 兩個線程同時轉移一個對象,一個成功,一個失敗
線程T1先通路對象,轉移失敗,傳回原始對象;線程T2後通路對象,轉移成功,傳回目标空間的對象。圖7-19中為了說明這一結論直接使用指針指向了不同的對象,實際上是兩個線程得到的傳回對象位址不同。
在這種情況下就出現了問題,兩個不同的線程指向了兩個不同的對象,根據并發轉移的要求,需要保證目标空間不變性。對于T1出現的轉移失敗情況需要特殊處理,理想的運作狀态是T1也應該通路目标對象,同時在其他的線程轉移完成後再進行轉移失敗處理,如圖7-20所示。
圖7-20 理想的運作狀态
那麼該如何保證有這樣的狀态?目前,Shenandoah設計了一個特殊的機制,來處理轉移中可能遇到的失敗情況。具體步驟如下:
1)當線程進入對象轉移時,增加計數器。
2)當線程成功轉移對象後,減少計數器。
3)當某一個線程出現轉移失敗後,等待其他線程完成轉移後才能繼續執行;線程會重新确定對象是否轉移,如果對象已經轉移,則擷取轉移後的對象。
(3)Shenandoah對JNI的優化
當Java應用執行的本地代碼中包含JNI Critical API時,因為本地代碼會操作Java堆空間中的記憶體對象,而垃圾回收執行時會移動對象,這兩個需求是沖突的,是以在執行JNI Critical API時會設定一個GCLocker标志,告訴垃圾回收暫停執行,直到JNI Critical API執行完畢才會再執行垃圾回收。這樣的設計的合理性是值得商榷的。
在Shenandoah中優化了這一設計,即在本地代碼執行JNI Critical API時仍然可以執行垃圾回收。其方法是,僅僅将JNI Critical API通路對象所在的記憶體固定(稱為Pinned),即垃圾回收可以繼續執行,當遇到記憶體固定的區域時不進行回收。由于Shenandoah采用分區設計,是以垃圾回收也是基于分區進行的。固定JNI Critical API通路對象所在記憶體可以将整個分區固定,隻要在垃圾回收時跳過這樣的分區即可。該優化在有較多JNI CriticalAPI的應用中有較好的效果。
目前JVM中僅Shenandoah支援該優化,實際上G1 GC和ZGC也是基于分區設計的,要想實作類似的優化并不困難。
(4)為什麼Shenandoah需要多種屏障
Shenandoah使用SATB屏障(本質是寫屏障)保證并發标記的正确性。在JDK 13之前,并發轉移階段使用讀屏障、寫屏障和比較屏障;在JDK 13之後,并發轉移階段使用Load屏障(本質是讀屏障)。在其他的垃圾回收器實作中,如JVM的ZGC、Android的Concurrent Copying都僅僅使用了Load屏障完成标記和轉移。那為什麼Shenandoah沒有統一多種屏障為一種?原因主要是不同的屏障性能不同。Shenandoah的一個主要維護者Aleksey Shipilev在介紹Shenandoah時比較過使用不同屏障的成本,如表7-3所示。
表7-3 SATB屏障和Load屏障在測試集上的成本比較
測試的基準是無屏障的情況。在表7-3中可以明顯看出Load屏障的成本更高。這也可能是Shenandoah選擇SATB算法進行并發标記的原因。
本文給大家講解的内容是JVM垃圾回收器詳解:Shenandoah,垃圾回收實作
- 下篇文章給大家講解的内容是JVM垃圾回收器詳解:Shenandoah,OpenJ9中的實時垃圾回收器Metronome介紹
- 感謝大家的支援!