JVM(五)垃圾回收器的前世今生

王磊的部落格發表於2019-02-13

全文共 2195 個字,讀完大約需要 8 分鐘。

如果垃圾回收的演算法屬於記憶體回收的方法論的話,那本文討論的垃圾回收器就屬於記憶體回收的具體實現。

因為不同的廠商(IBM、Oracle),實現的垃圾回收器各不相同,而本文要討論的是 Oracle 的 HotSpot 虛擬機器所使用的垃圾回收器。

常用垃圾回收器,如下圖所示:

常用垃圾回收器

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

其中相互連線的垃圾回收器,表示可以相互搭配使用。

新生代 And 老生代

目前常用的商用垃圾收集器都使用的是分代垃圾回收方式。

分代垃圾回收器把記憶體分為:新生代(Young Generation)和老生代(Tenured Generation),如下圖所示:

分代圖

(圖片來自fancydeepin)

預設情況下,新生代和老生代的記憶體比例是 1:2,該值可以通過 -XX:NewRatio 來設定。

新生代(Young Generation)

程式中的大部分物件都符合“朝生夕死”的特性,所以絕大數新建立的物件都會存放在新生代,除非是大物件會直接進入老生代。新生代採用的是複製演算法,這樣可以更高效的回收記憶體空間。

新生代有細分為:Eden、Form Survivor、To Survivor 三個區域,預設的比例是 8:1:1,可以通過 -XX:SurvivorRatio 來設定。

新生代垃圾回收的執行過程:

1、Eden 區 + From Survivor 區存活著的物件複製到 To Survivor 區;

2、清空 Eden 和 From Survivor 分割槽;

3、From Survivor 和 To Survivor 分割槽交換(From 變 To,To 變 From)。

老生代(Tenured Generation)

老生代垃圾回收的頻率比新生代低,存放的主要物件是:

1、新生代物件經過 N 次 GC 晉升到老年代。

可以通過設定 -XX:MaxTenuringThreshold=5 來設定,預設值是 15 次。

2、大物件直接儲存到老生代。

所謂的“大物件”指的是需要連續儲存空間的物件,比如:陣列。

當大物件在新生代儲存不下的時候,就需要分配擔保機制,把當前新生代的所有物件複製到老年代中,因為分配擔保機制需要涉及大量的複製,會導致效能問題,所有最好的方案是直接把大物件儲存到老生代中。

通過引數 -xx:PretrnureSizeThreshold 來設定大物件的值。

注意:該引數只有 Serial 和 ParNew 垃圾回收器有效。

Serial

Serial 最早的垃圾回收器,JDK 1.3.1 之前新生代唯一的垃圾回收器,使用的是單執行緒序列回收方式,在單 CPU 環境下效能較好,因為單執行緒執行不存線上程切換。

執行緒型別: 單執行緒

使用演算法: 複製演算法

指定收集器: -XX:+UseSerialGC

Serial Old

Serial 收集器的老年代版本,同樣也是單執行緒的。它有一個實用的用途作為CMS收集器的備選預案,後面介紹CMS的時候會詳細介紹。

執行緒型別: 單執行緒

使用演算法: 標記-整理

指定收集器: -XX:+UseSerialGC

ParNew

ParNew 其實就是 Serial 的多執行緒版本,可以和 Serial 共用很多控制引數,比如:-XX:SurvivorRatio , ParNew 可以和 CMS 配合使用。

parnew

(注:圖片來源於零壹技術棧)

執行緒型別: 多執行緒

使用演算法: 複製

指定收集器: -XX:+UseParNewGC

Parallel Scavenge

Parallel 和 ParNew 收集器類似,也是多執行緒的,但 Parallel 是吞吐量優先的收集器,GC停頓時間的縮短是以吞吐量為代價的,比如收集 100MB 的記憶體,需要 10S 的時間,CMS 則會縮短為 7S 收集 50 MB 的記憶體,這樣停頓的時間確實縮少了,但收集的頻率變大了,吞吐量就變小了。

執行緒型別: 多執行緒

使用演算法: 複製

指定收集器: -XX:+UseParallelGC

Parallel Old

Parallel Old 是 Parallel 的老生代版本,同樣是吞吐量優先的收集器。

執行緒型別: 多執行緒

使用演算法: 標記-整理

指定收集器: -XX:+UseParallelOldGC

CMS

CMS(Concurrent Mark Sweep)一種以獲得最短停頓時間為目標的收集器,非常適用B/S系統。

使用 Serial Old 整理記憶體。

CMS 執行過程:

CMS

(注:圖片來源於零壹技術棧)

1、初始標記

標記 GC Roots 直接關聯的物件,需要 Stop The World 。

2、併發標記

從 GC Roots 開始對堆進行可達性分析,找出活物件。

3、重新標記

重新標記階段為了修正併發期間由於使用者進行運作導致的標記變動的那一部分物件的標記記錄。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要 Stop The World 。

4、併發清除

除垃圾物件。

CMS 缺點:

1、對 CPU 資源要求敏感。

CMS 回收器過分依賴於多執行緒環境,預設情況下,開啟的執行緒數為(CPU 的數量 + 3)/ 4,當 CPU 數量少於 4 個時,CMS 對使用者本身的操作的影響將會很大,因為要分出一半的運算能力去執行回收器執行緒。

2、CMS無法清除浮動垃圾。

浮動垃圾指的是CMS清除垃圾的時候,還有使用者執行緒產生新的垃圾,這部分未被標記的垃圾叫做“浮動垃圾”,只能在下次 GC 的時候進行清除。

3、CMS 垃圾回收會產生大量空間碎片。

CMS 使用的是標記-清除演算法,所有在垃圾回收的時候回產生大量的空間碎片。

注意:CMS 收集器中,當老生代中的記憶體使用超過一定的比例時,系統將會進行垃圾回收;當剩餘記憶體不能滿足程式執行要求時,系統將會出現 Concurrent Mode Failure,臨時採用 Serial Old 演算法進行清除,此時的效能將會降低。

執行緒型別: 多執行緒

使用演算法: 標記-清除

指定收集器: -XX:+UseConcMarkSweepGC

G1

G1 GC 這是一種兼顧吞吐量和停頓時間的 GC 實現,是 JDK 9 以後的預設 GC 選項。G1 可以直觀的設定停頓時間的目標,相比於 CMS GC,G1 未必能做到 CMS 在最好情況下的延時停頓,但是最差情況要好很多。

G1 GC 仍然存在著年代的概念,但是其記憶體結構並不是簡單的條帶式劃分,而是類似棋盤的一個個 region。Region 之間是複製演算法,但整體上實際可看作是標記 - 整理(Mark-Compact)演算法,可以有效地避免記憶體碎片,尤其是當 Java 堆非常大的時候,G1 的優勢更加明顯。

G1

G1 吞吐量和停頓表現都非常不錯,並且仍然在不斷地完善,與此同時 CMS 已經在 JDK 9 中被標記為廢棄(deprecated),所以 G1 GC 值得深入掌握。

G1 執行過程:

1、初始標記

標記 GC Roots 直接關聯的物件,需要 Stop The World 。

2、併發標記

從 GC Roots 開始對堆進行可達性分析,找出活物件。

3、重新標記

重新標記階段為了修正併發期間由於使用者進行運作導致的標記變動的那一部分物件的標記記錄。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要 Stop The World 。

4、篩選回收

首先對各個 Region 的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來制定回收計劃。這個階段可以與使用者程式一起併發執行,但是因為只回收一部分 Region,時間是使用者可控制的。

執行緒型別: 多執行緒

使用演算法: 複製、標記-整理

指定收集器: -XX:+UseG1GC(JDK 7u4 版本後可用)

參考

《深入理解Java虛擬機器》

《垃圾回收的演算法與實現》

最後

關注公眾號,傳送“gc”關鍵字,領取《垃圾回收的演算法與實現》學習資料。

JVM(五)垃圾回收器的前世今生

JVM(五)垃圾回收器的前世今生

相關文章