Java 經典垃圾回收器詳解

語言: CN / TW / HK

highlight: atom-one-dark theme: fancy


這是我參與11月更文挑戰的第8天,活動詳情檢視:2021最後一次更文挑戰

垃圾回收器效能指標

  • 吞吐量:程式執行時間佔總執行時間(總執行時間=程式執行時間+垃圾回收時間)的比例,垃圾回收時間越少,吞吐量越高;
  • 暫停時間:STW的時間;
  • 記憶體佔用:Java堆所佔的大小。

以上三點構成不可能三角,即一款垃圾回收器不可能同時滿足三點。隨著硬體水平的提升,記憶體佔用不再是我們關注的重點,評估垃圾回收器效能時,重點關注吞吐量和暫停時間。吞吐量和暫停時間是相互矛盾的,目前我們追求的效果是:在最大吞吐量優先的情況下,減小暫停時間。

垃圾回收器發展歷史

  • 1999年JDK 1.3.1 釋出第一款序列方式的Serial GC,ParNew垃圾回收器是Serial回收器的多執行緒版本;
  • 2002年2月26,Parallel GC和Concurrent Mark Sweep GC(CMS)跟隨JDK 1.4.2一起釋出;
  • Parallel GC在JDK 1.6後稱為HotSpot預設GC;
  • 2012年,在JDK 1.7u4版本中,G1可用;
  • 2017年,JDK 9中,G1成為預設垃圾回收器,CMS被標記為過時;
  • 2018年3月,JDK 10中提升G1並行性;
  • 2018年9月,JDK 11引入了Epsilon垃圾回收器,同時引入ZGC(實驗版本);
  • 2019年3月,JDK 12釋出,增強G1,並引入Shenandoah GC(實驗版本);
  • 2019年9月,JDK 13釋出,增強ZGC;
  • 2020年3月,JDK 14釋出,刪除CMS,拓展ZGC在MAC和Windows上的應用。

垃圾回收器組合

7款經典垃圾回收器間的組合關係:

gc07.png

說明:

  1. 兩個回收器間有連線,說明它們可以搭配使用;
  2. Serial Old作為CMS出現“Concurrent Mode Failure”失敗的後備預案;
  3. G1可用於新生代和老年代;
  4. 紅色虛線連線:JDK 8將這兩組組合宣告為廢棄,並在JDK 9中完全移除;
  5. 綠色虛線連線:JDK 14中,棄用了該組合;
  6. 綠色虛線邊框:JDK 14中,刪除了CMS。

預設垃圾回收器檢視

編寫一段簡單的java程式:

java public class Test { public static void main(String[] args) { System.out.println("hello"); } }

新增-XX:+PrintCommandLineFlagsJVM引數配置,在JDK 8環境下程式輸出:

java -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC hello

-XX:+UseParallelGC說明JDK 8預設的垃圾回收器為Parallel。

在JDK 9環境下輸出:

java -XX:G1ConcRefinementThreads=10 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC hello

-XX:+UseG1GC說明JDK 9預設的垃圾回收器為G1。

經典垃圾回收器介紹

Serial、Serial Old回收器

Serial垃圾回收器為單執行緒序列回收器,為HotSpot中Client模式下預設的新生代垃圾回收器,採用複製演算法、序列回收和STW機制進行記憶體回收;

Serial Old垃圾回收器為Serial提供的老年代垃圾回收器,採用標記壓縮演算法、序列回收和STW機制進行記憶體回收:

  • Serial Old是執行在Client模式下預設的老年代垃圾回收器;
  • Serial Old在Server模式下主要有兩個用途:與新生代的Parallel Scavenge配合使用;作為老年代CMS回收器的後備垃圾收集方案。

Serial適用於執行在Client模式下的虛擬機器或者記憶體不大(幾十MB到一兩百MB)的環境下,因為是序列的,有較長時間的STW,所以並不適用於要求快響應、互動較強的應用。

可以通過XX:+UseSerialGC引數啟用Serial回收器,表示新生代使用Serial,老年代使用Serial Old。

ParNew回收器

ParNew是Parallel New兩個詞的簡寫,是Serial的多執行緒版本垃圾回收器。ParNew是很多JVM執行在Server模式下新生代的預設垃圾回收器,採用複製演算法,並行回收和STW機制進行記憶體回收。

可以通過XX:+UseParNewGC引數啟用ParNew回收器,表示新生代使用ParNew,老年代不受影響。

Serial、ParNew搭配Serial Old回收器示意圖:

gc08.jpg

圖片來自於https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/691189/

Parallel、Parallel Old回收器

Parallel Scavenge回收器也是作用於新生代,同樣採用複製演算法,並行回收和STW機制。

Parallel Scavenge和ParNew對比:

  • Parallel Scavenge為吞吐量優先的垃圾回收器;
  • Parallel Scavenge具有自適應調節策略。

JDK 1.6提供了用於老年代的並行垃圾回收器 —— Parallel Old回收器,用於替代Serial Old回收器。Parallel採用標記壓縮、並行回收和STW機制。

可以通過-XX:+UseParallelGC指定新生代使用Parallel Scavenge回收器;-XX:+UseParallelOldGC指定老年代使用Parallel Old回收器,它們是成對存在的,開啟一個另一個也會開啟。

此外還可以通過-XX:ParallelGCThreads=設定並行回收器的執行緒數:

  • 預設情況下,當CPU數量小於8個時,-XX:ParallelGCThreads=的值等於CPU數量;
  • 當CPU數量大於8個,-XX:ParallelGCThreads=的值等於3+5*CPU_COUNT/8

-XX:+UseAdaptiveSizePolicy開啟Parallel Scavenge的自適應調節策略:

  • 該模式下,年輕代大小、伊甸園區和倖存者區的比例、晉升老年代的物件年齡閾值都會自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。

CMS回收器

JDK 1.5 HotSpot推出了一款真正意義上的併發回收器 —— CMS(Concurrent-Mark-Sweep),第一次實現了讓垃圾回收執行緒和使用者執行緒同時工作。CMS的關注點在於儘可能縮短垃圾收集時使用者執行緒停頓的時間。

CMS作為一款老年代的垃圾回收器,不能和新生代垃圾回收器Parallel Scavenge搭配使用,只能和ParNew或者Serial搭配使用。

CMS回收器示意圖:

gc09.png

圖片來自於https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/691189/

主要分為以下幾個步驟:

  1. 初始標記(Initial-Mark):所有使用者執行緒暫停(STW),這個階段僅僅標記出GC Roots能直接關聯到的物件,所以速度非常快,STW時間很短;
  2. 併發標記(Concurrent-Mark):該階段從GC Roots直接關聯物件開始遍歷整個物件鏈,雖然這個過程耗時較長,但並不需要暫停使用者執行緒,併發執行,沒有STW;
  3. 重新標記(Remark):由於上一步使用者執行緒也在執行,所以這一步用於修正因使用者執行緒繼續執行而導致標記發生變動的那一部分物件的標記記錄。這個階段會比初始標記階段耗時長一點,但遠比並發標記階段低;
  4. 併發清除(Concurrent-Sweep):該階段清理刪除垃圾,回收空間。由於沒有移動物件,所以該階段也不需要STW。

CMS的優缺點都很明顯:

優點:

  • 併發收集;
  • 低延遲。

缺點:

  • 會產生碎片。因為清理階段使用者線執行緒還在執行,所以只能採用不移動物件的標記-清除演算法,而該演算法會產生碎片問題;
  • 對CPU資源敏感。CPU資源除了用於使用者執行緒外,還需分配一部分用於處理垃圾回收,降低了吞吐量;
  • 無法處理浮動垃圾。併發標記階段,使用者執行緒並未停止,該階段也會產生垃圾, CMS無法對這些垃圾進行標記,只能留到下次GC時處理。

此外,CMS在回收過程中,因為使用者執行緒並沒有中斷,所以還需確保使用者執行緒有足夠的記憶體可用。換句話說,CMS回收器不能等老年代即將被填滿時才去回收,而應當堆記憶體使用率到達一定閾值時,便開始進行回收。如果CMS執行期間預留記憶體不足,就會出現一次“Concurrent Mode Failure”失敗,虛擬機器會啟動後備方案,臨時啟用Serial Old回收器來完成老年代的垃圾回收。

CMS回收器可設定引數:

  • -XX:+UseConcMarkSweepGC,開啟CMS GC,開啟後,-XX:+UseParNewGC會自動開啟;
  • -XX:CMSInitiatingOccupanyFraction=,設定堆記憶體使用率閾值,一旦達到這個閾值,CMS開始進行回收(JDK5及之前,預設值為68,JDK6及以上版本預設值為92%);
  • -XX:+UseCMSCompactAtFullCollection,指定在CMS回收完老年代後,對記憶體空間進行壓縮處理,以避免碎片化問題;
  • -XX:CMSFullGCsBeforeCompaction,設定執行多少次CMS GC後,對記憶體空間進行壓縮整理;
  • -XX:ParallelCMSThreads=,設定CMS的執行緒數。預設啟動的執行緒數為(ParallelGCThreads+3)/4。我們知道,當CPU個數小於8時,ParallelGCThreads的預設值為CPU個數,所以對於一個8核CPU,預設啟動的CMS執行緒數為3,換句話說只有62.5%的CPU資源用於處理使用者執行緒。所以CMS不適合吞吐量要求高的場景。

G1回收器

G1(Garbage First)回收器把堆記憶體分割成很多不相關的區域(region,物理上不連續),使用不同區域來表示伊甸園區,倖存者區和老年代。

G1會避免對整個Java堆進行垃圾收集,它會跟蹤各個region裡垃圾回收的價值大小(回收所獲得的空間大小及所需時間的經驗值),在後臺維護一個優先列表,每次根據允許收集時間,優先回收價值最大的region。

region的說明

gc10.png

圖片來自於https://tech.meituan.com/2016/09/23/g1.html

  • E表示伊甸園區,S表示倖存者區、O表示老年代,空白表示未使用的記憶體區域;
  • 一個region在同一時間內只能屬於一種角色;
  • G1新增了一個全新的記憶體區域——Humongous,主要用於存放大物件。

G1回收垃圾過程如下圖所示:

gc11.png

圖片來自於https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/691189/

主要分為以下幾個步驟:

  1. 初始標記:僅僅是標記GC Roots能直接關聯的物件,需要STW,但這個過程非常快;
  2. 併發標記:從GC Roots出發,對堆中物件進行可達性分析,找出存活物件,該階段耗時較長,但是可與使用者執行緒併發執行;
  3. 最終標記:主要修正在併發標記階段因為使用者執行緒繼續執行而導致標記記錄產生變動的那一部分物件的標記記錄,需要STW;
  4. 篩選回收:將各個region分割槽的回收價值和成本進行排序,根據使用者所期望的停頓時間制定回收計劃。這階段停頓使用者執行緒,STW。

G1回收器的優缺點:

優點:

  • 並行與併發;
  • 分代收集,可以採用不同的演算法處理不同的物件;
  • 空間整合,標記壓縮演算法意味著不會產生記憶體碎片;
  • 可預測的停頓時間,能讓使用者明確指定一個長度為M毫秒時間片段內,消耗在垃圾回收的時間不超過N毫秒(根據優先列表優先回收價值最大的region)。

缺點:

  • 在小記憶體環境下和CMS相比沒有優勢,G1適合大的堆記憶體;
  • 在使用者程式執行過程中,G1無論是為了垃圾回收產生的記憶體佔用,還是程式執行時的額外執行負載都要比CMS高。

G1回收器相關引數設定:

  • -XX:+UseG1GC,開啟G1 GC;
  • -XX:G1HeapRegionSize=,設定region的大小。值為2的冪,範圍是1MB到32MB之間,目標是根據最小堆記憶體大小劃分出約2048個區域。所以如果這個值設定為2MB,那麼堆最小記憶體大約為4GB;
  • -XX:MaxGCPauseMillis=,設定期望達到的最大GC停頓時間指標(JVM會盡力實現,但不保證達到),預設值為200ms;
  • -XX:ParallelGCThread=,設定STW時GC執行緒數值,最多設定為8;
  • -XX:ConcGCThreads=,設定併發標記的執行緒數,推薦值為ParallelGCThread的1/4左右;
  • -XX:InitiatingHeapOccupancyPercent=,設定觸發併發GC週期的Java堆佔用率閾值,超過這個值就觸發GC,預設值為45。

總結

上面這幾款經典的垃圾回收器各有特點,具體使用的時候需要根據具體的情況選用不同的垃圾回收器:

gc12.png

| 垃圾回收器 | 分類 | 作用位置 | 使用演算法 | 特點 | 適用場景 | | :----------- | :--------- | :------------- | :--------------------- | :----------- | :----------------------------------- | | Serial | 序列 | 新生代 | 複製演算法 | 響應速度優先 | 適用於單CPU環境下的Client模式 | | ParNew | 並行 | 新生代 | 複製演算法 | 響應速度優先 | 多CPU環境Server模式下與CMS配合使用 | | Parallel | 並行 | 新生代 | 複製演算法 | 吞吐量優先 | 適用於後臺運算而不需要太多互動的場景 | | Serial Old | 序列 | 老年代 | 標記-壓縮演算法 | 響應速度優先 | 單CPU環境下的Client模式 | | Parallel Old | 並行 | 老年代 | 標記-壓縮演算法 | 吞吐量優先 | 適用於後臺運算而不需要太多互動的場景 | | CMS | 併發 | 老年代 | 標記-壓縮演算法 | 響應速度優先 | 適用於網際網路或B/S業務 | | G1 | 並行與併發 | 新生代、老年代 | 複製演算法 標記-壓縮演算法 | 響應速度優先 | 面向服務端應用 |

新垃圾回收器

Epsilon回收器、Shenandoah回收器、ZGC回收器