天天看點

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

作者:大資料架構師

并行回收

并行回收(Parallel Scavenge Garage Collection,簡稱ParallelGC、PS GC或PS)是JVM吞吐率最高的垃圾回收器之一。

并行回收也采用分代記憶體管理方式,和串行回收采用的算法基本一緻,但在實作時有不少特色,主要表現如下:

1)具有獨特的記憶體管理機制,不僅支援新生代中Eden和Survivor大小的自适應調整,還支援兩個代大小的自适應調整。

2)它是第一個支援NUMA-Aware的垃圾回收器,并且可以根據應用的運作情況自适應地調整NUMA所用的大小。

3)新生代采用并行複制算法,類似于第4章的ParNew;如果在Minor GC發生後仍然無法滿足Mutator的記憶體請求,則會采用并行标記壓縮算法對整個記憶體進行回收,算法的并行化基于依賴樹結構實作。

下面着重介紹并行回收這3個特點。

記憶體管理

并行回收的記憶體管理與JVM中其他的垃圾回收器的記憶體管理都不相同。其主要原因是并行回收希望在運作時根據Mutator運作的情況動态地調整新生代和老生代的邊界。

根據記憶體分代後每個代的使用原則,Mutator運作時在新生代中配置設定對象,當新生代空間不足時執行Minor GC,并将長期存活的對象晉升到老生代。

分代的代際邊界可以調整,意味着:新生代可以增大/減少,而老生代可以減少/增大。但無論邊界如何調整,都不應該移動老生代的對象,否則邊界調整将導緻性能下降(或者表現為停頓時間增加)。

另外,新生代和老生代都是連續的虛拟位址,是以需要小心設計記憶體管理模型,既能滿足代際邊界的動态調整,又能滿足空間的連續配置設定。通常有兩種記憶體模型可以滿足上述要求,分别是:

1)新生代在前,老生代在後。

2)老生代在前,新生代在後。

對于第一種記憶體模型,新生代的記憶體配置設定方向是從低位址往高位址配置設定,而老生代的記憶體配置設定方向是從高位址往低位址配置設定,如圖5-1所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-1 新生代在前,老生代在後的記憶體管理模型

使用該模型,當執行完Minor GC後判斷是否需要調整邊界,如果需要,可以老生代使用的低位址作為邊界,并以此邊界作為新生代大小的上界控制新生代大小的調整。該模型的優點是新生代的管理和其他垃圾回收器的記憶體管理模型一緻,但是在老生代中配置設定記憶體需要從高位址往低位址方向配置設定。

另外,該模型對于Full GC的實作稍微有些複雜,在Full GC發生時需要将所有對象從高位址開始壓縮、整理。

對于第二種記憶體模型,老生代的記憶體配置設定方向為從低位址往高位址配置設定,而新生代的記憶體配置設定方向為從高位址往低位址配置設定,如圖5-2所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-2 老生代在前,新生代在後的記憶體管理模型

使用該模型,當執行完Minor GC後判斷是否需要調整邊界,如果需要,則可以根據老生代使用的高位址作為邊界,并以此邊界作為新生代大小的下界控制新生代大小的調整。該模型的優點是對Full GC友好。使用該模型,新生代位址從高位址往低位址方向配置設定,但由于記憶體配置設定使以總是以低位址作為起始位置,是以在配置設定記憶體時需要先計算記憶體大小,然後根據使用記憶體的邊界計算出低位址的起始位置。

使用這兩種模型都可以滿足調整新生代的大小的需求,也可以滿足在Minor GC晉升對象後動态調整老生代大小的需求。

記憶體管理模型

使用第二種模型實作的複雜度較低,是以并行回收記憶體模型是按照老生代在前、新生代在後的組織方式實作的。

對于一些應用來說,并不需要邊界調整這個功能。例如,應用新生代占用的記憶體相對固定,此時如果采用固定邊界,新生代和老生代在記憶體配置設定時都可以從低位址往高位址配置設定,進而減少新生代記憶體配置設定時額外的位址調整操作。

在并行回收中通過參數UseAdaptiveGCBoundary(預設值為false,表示不支援邊界浮動功能)控制應用是否需要支援浮動邊界功能。由于并行回收同時支援邊界固定和邊界浮動的功能,為了儲存代碼統一,在邊界固定的場景中也是老生代在前、新生代在後。邊界固定的記憶體管理模型如圖5-3所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-3 邊界固定的記憶體管理模型

而邊界浮動的記憶體模型如圖5-2所示,但是該特性在JDK 15中被移除,主要原因是邊界浮動可能導緻一些應用崩潰,而且該功能使用得較少,并且JVM的重心不再是并行回收,是以JVM開發團隊直接移除了該功能。

另外,在并行回收新生代中支援自适應調整Eden和Survivor分區的大小。該功能可以通過參數UseAdaptiveSizePolicy(預設值為true,表示支援調整)控制是否開啟。但是該功能也在一定程度上增加了記憶體管理的複雜性。根據複制算法的實作,将新生代劃分為3個空間,分别是Eden、To和From,如圖5-4所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-4 新生代的空間劃分

在複制算法執行結束後,To空間中儲存的是新生代中的所有活躍對象(已經晉升的對象除外),Eden和From空間都為空。其中Eden用于下一次Mutator對象配置設定,From和To空間交換,交換後To空間為空,To用于儲存下一次複制算法發現的活躍對象。此時新生代的記憶體布局如圖5-5所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-5 新生代的記憶體布局

由于From空間儲存的是活躍對象,當調整From和To空間的大小時,要避免移動From空間中的對象。是以在調整From和To空間大小的時候需要考慮To和From這兩個空間的順序。

如果垃圾回收後子空間的順序為Eden、To和From,那麼無論是擴大還是縮小From和To空間的大小,調整方式都比較簡單。隻需要保證調整後Eden+To的大小小于From的起始位址即可。

如果垃圾回收後空間的順序為Eden、From和To,那麼由于From空間有存活對象,需要保證Eden的結束位址小于From的起始位址(否則必須移動From空間中的對象),同時要求From空間調整後的大小大于現有存活對象的總大小。在該順序下調整From和To空間的大小,在調整後可能導緻記憶體有空洞(即有部分記憶體無法使用),例如将From的起始位址調整到比目前位址小的起始位址後(僅僅修改起始位址,但不移動對象),此時新的起始位址和老的起始位址之間就形成了空洞。

在複制算法的執行過程還可能會遇到轉移失敗的情況,即To空間和老生代都沒有足夠的空間來儲存新生代中标記的活躍對象。對于這種情況,一定有Eden和To空間不為空,說明發生了轉移失敗的情況。在這種情況下不會調整新生代子空間的大小,也就是說隻有成功執行複制算法的垃圾回收才可能嘗試調整新生代子空間的大小。

NUMA支援

并行回收是第一個支援NUMA-Aware的垃圾回收器。在Mutator請求記憶體的時候,如果打開UseNUMA特性,可以從Mutator運作的節點配置設定記憶體。使用NUMA-Aware的方式管理記憶體,可以加速Mutator對記憶體的通路,但是需要考慮多個NUMA節點共享一個Eden大小時該如何觸發垃圾回收的問題。一個常見的處理方法是“任意”一個NUMA節點的記憶體不足時,都會觸發垃圾回收。這樣的觸發方式最為簡單,但是每個NUMA節點上Mutator請求的記憶體并不相同,可能存在差異比較大的情況。是以并行回收設計了兩種方法來管理新生代記憶體:

1)每一個NUMA節點平分Eden的大小。

2)每一個NUMA節點根據Mutator使用的記憶體大小,動态地為每一個NUMA節點配置設定記憶體。

預設方式是,多個NUMA節點(例如共有n個NUMA節點)平均劃分Eden的空間,例如系統有4個NUMA節點,整體Eden為1GB,則每個NUMA節點管理的記憶體上限為256MB,當NUMA節點上記憶體使用達到256MB時就會觸發垃圾回收。NUMA平分Eden的管理方式如圖5-6所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-6 Eden的NUMA管理方式

而實際情況是,每個節點使用記憶體的速率并不相同,是以并行回收支援動态地調整NUMA節點的記憶體大小。并行回收提供一些參數來控制每個NUMA管理記憶體的大小,分别是:

1)參數UseAdaptiveNUMAChunkSizing,控制是否可以動态調整NUMA節點的記憶體大小。參數預設值為true,表示支援動态調整每個NUMA節點管理記憶體的大小。

2)參數AdaptiveSizePolicyReadyThreshold,當收集資料的次數達到該門檻值時才會調整NUMA節點的大小。參數預設值為5,表示隻有發生過5次MinorGC才會啟動動态調整NUMA節點大小的功能(該參數為開發參數,生産版本的JDK不能調整該參數)。

3)參數NUMAChunkResizeWeight,計算配置設定速率時使用。參數預設值為20,表示參數調整時曆史資料的權重。

4)參數NUMASpaceResizeRate,用于控制調整NUMA節點記憶體時的步幅。

該參數可防止某一個NUMA過多配置設定Eden。例如參數預設值為1GB,如果系統有4個NUMA節點,則步幅控制可以通過公式

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

來計算。

記憶體配置設定和GC觸發流程

一般來說,新生代主要響應Mutator的配置設定請求,老生代用于Minor GC晉升對象的配置設定。但是如果新生代和老生代劃分不合理,則可能存在頻繁觸發Minor GC而老生代仍然還有非常大的空間的情況。為此,并行回收嘗試在新生代無法滿足Mutator的配置設定請求時,在滿足一定條件時,在老生代中響應Mutator的記憶體請求。

在并行回收中每個Mutator也都有一個TLAB,記憶體請求先從TLAB中配置設定,當TLAB無法滿足Mutator的響應時,會嘗試從新生代中配置設定一塊新的TLAB(注意,如果Mutator請求的記憶體大小大于TLAB,會按照實際記憶體直接在新生代中配置設定)。

與3.2節介紹的配置設定順序稍有不同,在3.2節介紹串行回收和CMS的配置設定分為3個層次——無鎖配置設定、加鎖配置設定、垃圾回收後配置設定,配置設定的成本依次增加。在并行回收中,配置設定增加了一個層次,共分為4個層次:無鎖配置設定、加鎖配置設定、在老生代中配置設定、垃圾回收後配置設定。當然,在老生代中的配置設定嘗試需要滿足一定條件才可以進行。在老生代中配置設定的流程如圖5-7所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-7 額外增加的老生代配置設定政策

執行垃圾回收後記憶體配置設定的流程圖如圖5-8所示。

4000字搞懂并行回收記憶體管理模型+NUMA支援+記憶體配置設定和GC觸發流程

圖5-8 垃圾回收後記憶體配置設定流程圖

值得注意的是,并行垃圾回收中Full GC處理稍有不同,在Minor GC執行結束後,會判斷是否需要執行Full GC,如果需要,則執行Full GC(至于是否要清除引用,則依賴于引用的狀态)。是以一次Minor GC可能會觸發3次Full GC(其中兩次Full GC是Minor GC中的正常流程,第一次是Full GC不回收Java引用,第二次是Full GC回收Java引用,第三次是GC執行完成後額外觸發的Full GC)。在以下兩種情況下Minor GC會額外觸發第三次Full GC。

1)Minor GC執行失敗,即老生代無法滿足新生代晉升的需要。

2)預測的晉升對象大小大于老生代可用的記憶體。

本文給大家講解的内容是JVM垃圾回收器詳解:并行回收的記憶體管理

  1. 下篇文章給大家講解的内容是JVM垃圾回收器詳解:并行回收, Minor GC、Full GC
  2. 感謝大家的支援!

繼續閱讀