【深入理解JVM】:HotSpot垃圾收集器

相關概念

併發和並行

這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,它們可以解釋如下。

  • 並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另一個CPU上。

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因爲Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

  • 老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

吞吐量

吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即

吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。

虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

HotSpot垃圾收集器

在虛擬機規範中並沒有對垃圾回收器如何實現具體介紹,因此每個廠商的垃圾回收器可能會完全不同,但是我們介紹的是基於JDK1.7之後的Hotspot虛擬機(包括前面對Java虛擬機的介紹也是基於jdk1.7版本的)。在Hotspot中,虛擬機的收集器主要有下:

HotSpot垃圾收集器

可以看到垃圾收集器是按對象的分代來劃分的,可以用雙箭頭連接的垃圾收集器表示兩者可以配合使用。可以看到新生代垃圾收集器有Serial、ParNew、Parallel Scavenge,G1,屬於老年代的垃圾收集器有CMS、Serial Old、Parallel Old和G1.其中的G1是一種既可以對新生代對象也可以對老年代對象進行回收的垃圾收集器。然而,在所有的垃圾收集器中,並沒有一種普遍使用的垃圾收集器。在不同的場景下,每種垃圾收集器有各自的優勢。

Serial收集器

Serial收集器是最基本、發展歷史最悠久的收集器。它是一種單線程垃圾收集器,這就意味着在其進行垃圾收集的時候需要暫停其他的線程,也就是之前提到的”Stop the world“。雖然這個過程是在用戶不可見的情況下把用戶正常的線程全部停掉,聽起來有點狠,這點是很難讓人接受的。Serial、Serial Old收集器的工作示意圖如下:

Serial收集器

儘管由以上不能讓人接受的地方,但是Serial收集器還是有其優點的:簡單而高效,對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得較高的手機效率。到目前爲止,Serial收集器依然是Client模式下的默認的新生代垃圾收集器。

ParNew收集器

可ParNew收集器是Serial收集器的多線程版本,ParNew收集器的工作示意圖如下:

ParNew收集器

ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器。除去性能因素,很重要的原因是除了Serial收集器外,目前只有它能與CMS收集器配合工作。

但是,在單CPU環境中,ParNew收集器絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。然而,隨着可以使用的CPU的數量的增加,它對於GC時系統資源的有效利用還是很有好處的。

Parallel Scavenge收集器

Parallel Scavenge收集器是新生代垃圾收集器,使用複製算法,也是並行的多線程收集器。與ParNew收集器相比,很多相似之處,但是Parallel Scavenge收集器更關注可控制的吞吐量。吞吐量越大,垃圾收集的時間越短,則用戶代碼則可以充分利用CPU資源,儘快完成程序的運算任務。

Parallel Scavenge收集器使用兩個參數控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間

  • XX:GCRatio 直接設置吞吐量的大小。

直觀上,只要最大的垃圾收集停頓時間越小,吞吐量是越高的,但是GC停頓時間的縮短是以犧牲吞吐量和新生代空間作爲代價的。比如原來10秒收集一次,每次停頓100毫秒,現在變成5秒收集一次,每次停頓70毫秒。停頓時間下降的同時,吞吐量也下降了。

除此之外,Parallel Scavenge收集器還可以設置參數-XX:+UseAdaptiveSizePocily來動態調整停頓時間或者最大的吞吐量,這種方式稱爲GC自適應調節策略,這點是ParNew收集器所沒有的。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一個單線程收集器,採用“標記-整理算法”進行回收。其運行過程與Serial收集器一樣。

Serial Old收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法進行垃圾回收。其通常與Parallel Scavenge收集器配合使用,“吞吐量優先”收集器是這個組合的特點,在注重吞吐量和CPU資源敏感的場合,都可以使用這個組合。

Parallel Scavenge and Parrallel Old收集器組合

CMS收集器

CMS收集器(Concurrent Mark Sweep)的目標就是獲取最短回收停頓時間。在注重服務器的響應速度,希望停頓時間最短,則CMS收集器是比較好的選擇。

整個執行過程分爲以下4個步驟:

  • 初始標記
  • 併發標記
  • 重新標記
  • 併發清除

初始標記和重新標記這兩個步驟仍然需要暫停Java執行線程,初始標記只是標記GC Roots能夠關聯到的對象,併發標記就是執行GC Roots Tracing的過程,而重新標記就是爲了修正併發標記期間因用戶程序執行而導致標記發生變動使得標記錯誤的記錄。其執行過程如下:

CMS收集器

由上圖可知,整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,因此,總體上CMS收集器的內存回收過程是與用戶線程一起併發執行的。

CMS的優點很明顯:併發收集、低停頓。由於進行垃圾收集的時間主要耗在併發標記與併發清除這兩個過程,雖然初始標記和重新標記仍然需要暫停用戶線程,但是從總體上看,這部分佔用的時間相比其他兩個步驟很小,所以可以認爲是低停頓的。

儘管如此,CMS收集器的缺點也是很明顯的:

  • 對CPU資源太敏感,這點可以這麼理解,雖然在併發標記階段用戶線程沒有暫停,但是由於收集器佔用了一部分CPU資源,導致程序的響應速度變慢

  • CMS收集器無法處理浮動垃圾。所謂的“浮動垃圾”,就是在併發標記階段,由於用戶程序在運行,那麼自然就會有新的垃圾產生,這部分垃圾被標記過後,CMS無法在當次集中處理它們(爲什麼?原因在於CMS是以獲取最短停頓時間爲目標的,自然不可能在一次垃圾處理過程中花費太多時間),只好在下一次GC的時候處理。這部分未處理的垃圾就稱爲“浮動垃圾”

  • 由於CMS收集器是基於“標記-清除”算法的,前面說過這個算法會導致大量的空間碎片的產生,一旦空間碎片過多,大對象就沒辦法給其分配內存,那麼即使內存還有剩餘空間容納這個大對象,但是卻沒有連續的足夠大的空間放下這個對象,所以虛擬機就會觸發一次Full GC(這個後面還會提到)這個問題的解決是通過控制參數-XX:+UseCMSCompactAtFullCollection,用於在CMS垃圾收集器頂不住要進行FullGC的時候開啓空間碎片的合併整理過程。

G1收集器

G1(Garbage-First)收集器是現今收集器技術的最新成果之一,之前一直處於實驗階段,直到jdk7u4之後,才正式作爲商用的收集器。

與前幾個收集器相比,G1收集器有以下特點:

  • 並行與併發
  • 分代收集(仍然保留了分代的概念)
  • 空間整合(整體上屬於“標記-整理”算法,不會導致空間碎片)
  • 可預測的停頓(比CMS更先進的地方在於能讓使用者明確指定一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒)

此外,G1收集器將Java堆劃分爲多個大小相等的Region(獨立區域),新生代與老年代都是一部分Region的集合,G1的收集範圍則是這一個個Region(化整爲零)。

G1的工作過程如下:

  • 初始標記(Initial Marking)
  • 併發標記(Concurrent Marking)
  • 最終標記(Final Marking)
  • 篩選回收(Live Data Counting and Evacuation)

初始標記階段僅僅只是標記一下GC Roots能夠直接關聯的對象,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段的用戶程序併發運行的時候,能在正確可用的Region中創建對象,這個階段需要暫停線程。併發標記階段從GC Roots進行可達性分析,找出存活的對象,這個階段食慾用戶線程併發執行的。最終標記階段則是修正在併發標記階段因爲用戶程序的併發執行而導致標記產生變動的那一部分記錄,這部分記錄被保存在Remembered Set Logs中,最終標記階段再把Logs中的記錄合併到Remembered Set中,這個階段是並行執行的,仍然需要暫停用戶線程。最後在篩選階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間制定回收計劃。整個執行過程如下:

G1收集器

垃圾收集器常用參數總結

參數1

client/serrver端不同的GC方式:

client server

Sun JDK HotSpot虛擬機GC組合方式:

sun jkd

參考
1、周志明,深入理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社

發佈了117 篇原創文章 · 獲贊 184 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章