天天看點

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

作者:大資料架構師

并發的老生代回收

在前面提到,并發老生代回收是為了解決停頓時間過長的問題,是以在設計中采用了連結清單的方式管理空閑記憶體。同時為了提高配置設定的效率,實作了樹和連結清單複合的管理方式。前文也提到,老生代在具體實作時采用了更進一步的優化,以減少額外記憶體占用。雖然這些優化方式減少了額外的記憶體占用,但加大了實作并發的複雜性。下面詳細讨論。

記憶體管理

老生代為了滿足小對象的高效配置設定,在樹結構的基礎上又引入了一個類似緩存的機制,專門用于處理小對象的配置設定。具體的想法是,針對常見的小對象預先配置設定一個緩存清單,稱為IndexedFreeList,對象配置設定時,優先從緩存清單中配置設定,當緩存清單中沒有相應大小的記憶體塊時,再從一個較大的塊中擷取;當較大的記憶體還無法滿足記憶體請求時,再從樹結構中擷取記憶體塊。

是以緩存清單用于響應小對象的配置設定,樹結構用于響應大對象或者緩存清單的配置設定。具體思路是,定義[0,256]共計257字的緩存清單,如圖4-18所示。

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

圖4-18 多條固定長度的連結清單管理記憶體

在圖4-18中,字長小于3字的緩存清單實際上并未使用,因為在JVM内部一個對象最少占用3字,主要原因是每個對象都必須有一個對象頭,而對象頭的大小為2字,而為了差別不同的空對象,會為每個空對象增加一個額外的字空間,是以一個對象的大小最小為3字。

另外,老生代還設定了一塊專門用于處理超小對象的配置設定緩存空間,該緩存稱為LinearAllocBlock(簡稱LAB)。其思路是,當緩存清單無法滿足超小對象的配置設定請求時,從該緩存中配置設定對象,超小對象的上限為16字。設計超小對象的緩存的目的有兩個:加速超小對象的配置設定效率;減少超小對象占用空閑清單,避免超小對象導緻的記憶體碎片問題。LinearAllocBlock也是一個記憶體塊,如圖4-19所示。

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

圖4-19 避免碎片化的小對象緩存[1]

從LAB配置設定的對象通常都是小對象(小于16字),需要将已經配置設定的對象統計到IndexedFreeList的數組中(在合并時需要這些資訊)。如果對象被釋放,可以直接放到IndexedFreeList中。注意,LAB預配置設定的空間從二叉樹中擷取,但是LAB僅僅是預配置設定空間,是以LAB的記憶體不能和空閑的記憶體塊合并。

而大記憶體塊采用的存儲結構是如圖4-4所示的二叉樹+連結清單的管理形式。

老生代存在3種管理方式,分别為固定清單、LAB和二叉樹。記憶體配置設定的流程圖如圖4-20所示。

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

圖4-20 老生代對象配置設定流程圖

從另一個角度出發,當發現記憶體空閑時,需要使用指針将記憶體塊關聯到空閑清單,這需要占用記憶體。而記憶體塊空閑或者記憶體塊被配置設定給對象這兩種情況不可能同時存在,是以可以将記憶體塊前面的空間進行複用:當記憶體塊用于對象配置設定時,記憶體塊的空間被識别為中繼資料;當記憶體塊位于空閑清單中時,記憶體塊的空間被用作指針(維護連結清單或者樹結構)。空間的複用減少了額外記憶體的占用。

在介紹記憶體複用前先來回顧一下對象的記憶體布局。關于Java的對象,在JVM内部使用instanceOop來表示,其記憶體布局如圖4-21所示。

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

圖4-21 JVM對象記憶體布局示意圖

其中markoop是中繼資料資訊,可以儲存hashcode、鎖資訊、gc狀态資訊等;klass是指向Java對象所屬的Java類的指針;field是Java對象的成員變量。

在老生代的記憶體管理中使用兩種類型的資料結構,分别是二叉樹和連結清單。其中連結清單直接管理空閑記憶體塊,樹管理空閑連結清單。連結清單管理時需要連結清單節點(list node)來輔助,連結清單節點至少需要兩個子指針,分别指向前序節點和後序節點,當空閑塊長度未知時還需要一個表示長度的字段。二叉樹需要樹節點(tree node)輔助管理,樹節點至少需要3個字段,分别是指向父節點、左子樹和右子樹的指針。比較樹節點和連結清單節點的結構可以将其共同抽象為使用3個字段的結構,包含大小和兩個指針,如圖4-22所示。

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

圖4-22 樹和連結清單管理結構示意圖

從二叉樹和連結清單的管理結構來看,每個樹節點和連結清單節點都需要占用額外的記憶體空間。在樹節點和連結清單節點比較多的場景中,會因為輔助的記憶體管理結構帶來不小的額外空間消耗。另外,還需要考慮這些空間消耗是使用本地記憶體還是堆記憶體,如何配置設定和釋放這些記憶體,確定不會出現記憶體不足或者記憶體碎片等問題。為了解決上述問題,CMS的老生代在堆記憶體中配置設定管理結構的記憶體,同時将管理結構和對象頭進行複用。首先記憶體用于對象時表示記憶體已使用,而記憶體用于管理結構時表示記憶體是空閑的,兩者的狀态是不會重複的;其次比較instanceOop和管理結構,可以發現它們都至少包含了2字的有效資訊,是以可以直接将同一記憶體的字段複用。

記憶體用于Java對象配置設定時,記憶體塊直接解析為instanceOop的結構。

記憶體空閑時,被複用為管理結構。

通過複用可以減少因記憶體管理帶來的記憶體消耗。但是這也為實作帶來了一定的複雜性。

圖4-22僅僅是示範記憶體可以複用的一個抽象結構示意圖。要實作記憶體複用,除了對資料結構需要仔細斟酌外,實作中還需要諸多的考量。比如從二叉樹擷取一個空閑記憶體塊時,該選取哪個記憶體塊?

通過上面的介紹,可以發現樹節點和連結清單節點有所不同,但是樹節點和連結清單節點都描述了相同大小的空閑記憶體塊。是以樹節點本質上是第一個連結清單的節點,但是為了友善管理,将樹節點和連結清單區分開來。在使用記憶體塊時,優先使用連結清單的記憶體塊,主要原因是當樹節點被用于配置設定時,需要對二叉樹進行重構(保持平衡),成本比較高;隻有當樹節點關聯的連結清單全部使用完後才會使用樹節點。

需要注意的是,JVM中樹節點管理記憶體塊均大于256字,是以一個樹節點同時包含一個連結清單節點并不困難。在JVM中,樹節點的結構被稱為TreeList,

其記憶體布局如圖4-23所示。

JVM垃圾回收器詳解并發标記清除回收,并發的老生代回收記憶體管理

圖4-23 JVM中樹節點記憶體管理示意圖

當然,記憶體空間複用可以減少額外記憶體消耗,但是也增加了額外的複雜性。第一個方面表現在記憶體塊的配置設定上,記憶體塊既需要滿足對象對齊要求,又需要滿足接傳入連結表的要求。在32位系統中要求剩餘的記憶體塊必須大于3字。

這在某些情況下會帶來一些問題。例如記憶體塊大小為1022字,遇到一個請求為1020字。從配置設定的角度來看,1022字完全可以響應1020字大小的請求,隻不過在滿足配置設定請求後,還剩餘2字。但是由于2字的記憶體塊無法接入記憶體連結清單中,是以JVM會拒絕這次配置設定請求。

第二個方面表現在代碼實作的複雜性上。老生代回收是并發執行,意味着Mutator可以在老生代回收的過程中在老生代中配置設定對象。配置設定對象實際上包含兩個動作:第一是記憶體配置設定的請求;第二是對象的初始化。

但是由于并發運作,很有可能Mutator在完成記憶體配置設定後,尚未完成對象的初始化時,GC線程通路了這一記憶體塊,然而由于對象尚未完成初始化,即對象的中繼資料尚未正确設定,我們知道對象的中繼資料中包含了對象的大小,對于這樣的情況則無法通過中繼資料擷取對象的大小。如果需要對象的大小,該如何處理?這就需要額外的代碼來處理這樣的情況。關于這一問題的較長的描述和解決方法在後文介紹。

本文給大家講解的内容是JVM垃圾回收器詳解:并發标記清除回收,并發的老生代回收-記憶體管理

  1. 下篇文章給大家講解的内容是JVM垃圾回收器詳解:并發标記清除回收,并發的老生代回收-标記清除算法概述及并發算法觸發時機
  2. 感謝大家的支援!

繼續閱讀