文章目錄
- 前言
- 1.串行(Serial 收集器/Serial Old 收集器)
- Serial 收集器
- Serial Old 收集器
- 相關參數
- -XX:+UseSerialGC
- 2.吞吐量優先(Parallel Scavenge 收集器/Parallel Old 收集器)
- Parallel Scavenge 收集器
- Parallel Old 收集器
- 相關參數
- -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
- -XX:+UseAdaptiveSizePolicy
- -XX:GCTimeRatio=ratio
- -XX:MaxGCPauseMillis=ms
- -XX:ParallelGCThreads=n
- 3.響應時間優先(ParNew 收集器/CMS 收集器)
- CMS 收集器
- 相關參數
- -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
- -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
- -XX:CMSInitiatingOccupancyFraction=percent
- -XX:+CMSScavengeBeforeRemark
- 4.G1(Garbage First收集器)
- 前言
- 介紹
- G1 VS CMS
- 1.Young Collection
- 2.Young Collection + CM
- 3.Mixed Collection
- 4.Full GC
- 5.Remark(重新標記階段)
- ZGC 收集器
前言
如果說回收(收集)算法是內存回收的方法論,那么垃圾回收(收集)器就是內存回收的具體實現。
雖然有各種的收集器,但并非要挑選出一個最好的收集器。因為直到現在為止還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那么我們的 HotSpot 虛擬機就不會實現那么多不同的垃圾收集器了。
JDK 默認垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):
- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK22: G1
1.串行(Serial 收集器/Serial Old 收集器)
Serial 收集器
Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。
新生代采用標記-復制算法,老年代采用標記-整理算法。

因為Serial(串行)收集器出現的 Stop The World 帶來的不良用戶體驗,所以在后續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。
Serial 收集器優點:它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對于運行在 Client 模式下的虛擬機來說是個不錯的選擇。
Serial Old 收集器
Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:
一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備方案。

相關參數
-XX:+UseSerialGC
-XX:+UseSerialGC 實際上是指定使用Serial(年輕代)和Serial Old(老年代)的組合。這種設置適合于數據量較小、對應用的停頓時間要求不高且資源受限(如CPU核心數量有限)的環境。
2.吞吐量優先(Parallel Scavenge 收集器/Parallel Old 收集器)
JDK1.8中默認使用收集器為Parallel Scavenge + Parallel Old收集器組合
使用 java -XX:+PrintCommandLineFlags -version 命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默認使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 參數,則默認指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 來禁用該功能
Parallel Scavenge 收集器
Parallel Scavenge 收集器是使用標記-復制算法的多線程收集器,Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。
所謂吞吐量就是 CPU 中用于運行用戶代碼的時間與 CPU 總消耗時間的比值。

Parallel Scavenge 收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解,手工優化存在困難的時候,使用 Parallel Scavenge 收集器配合自適應調節策略,把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
新生代采用標記-復制算法,老年代采用標記-整理算法。

Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。

相關參數
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
解釋:UseParallelGC:工作于新生代,復制算法
UseParallelOldGC:工作于老年代,標記整理算法
注意:開啟其中一個會自動開啟另一個
-XX:+UseAdaptiveSizePolicy
- 功能:
- 啟用自適應大小調整(新生代)策略。該策略會根據運行時的統計信息動態調整新生代(Young Generation)的大小(Eden 區和 Survivor 區)、晉升老年代的閾值等內存分配參數。JVM 會根據當前垃圾回收的性能表現自動調整這些參數,以達到更好的性能優化效果。
- 當啟用此參數時,JVM 會根據應用程序的行為和垃圾回收統計數據,嘗試找到最優的內存分配比例和晉升閾值,而無需手動指定諸如 -Xmn(新生代大小)或 -XX:SurvivorRatio 等參數。
-XX:GCTimeRatio=ratio
- 功能:
- 該參數用于設置吞吐量目標,它是一個控制垃圾回收時間占總運行時間比例的參數。它表示允許的最大垃圾回收時間與總運行時間的比例。
- 具體計算方式為 1 / (1 + ratio)。例如,如果你設置 ratio = 19,那么最大總垃圾回收時間占總運行時間的比例為 1 / (1 + 19) = 5%。
- JVM 會盡量調整垃圾回收器的工作,以確保垃圾回收時間不超過總運行時間的這個比例,從而保證應用程序的吞吐量,提高程序執行效率。
-XX:MaxGCPauseMillis=ms
- 功能:
- 設定垃圾回收的最大暫停時間目標,單位是毫秒。JVM 會盡力將垃圾回收的暫停時間控制在這個目標值以內。默認200ms
- 當設置此參數后,JVM 會調整堆內存大小、新生代和老年代的比例等,使每次垃圾回收的暫停時間盡可能不超過該值。
- 然而,需要注意的是,為了達到這個目標,可能會導致更頻繁的垃圾回收操作,或者減少堆內存的使用量,因此需要根據實際情況進行權衡,避免因過度追求低暫停時間而影響整體性能。
注意:-XX:MaxGCPauseMillis=ms和-XX:GCTimeRatio=ratio會有沖突。
當調整 -XX:MaxGCPauseMillis 以追求更短的暫停時間時,可能會影響 -XX:GCTimeRatio 所期望的吞吐量目標。
因為為了達到更短的暫停時間,可能會頻繁進行垃圾回收,這會增加垃圾回收的總時間,導致垃圾回收時間占總運行時間的比例增加,從而可能無法達到 -XX:GCTimeRatio 設定的吞吐量目標。
反之,當調整 -XX:GCTimeRatio 以提高吞吐量時(一般會將堆內存增大),可能會導致單次垃圾回收的暫停時間變長(堆內存變大導致單次時間變長),從而可能超過 -XX:MaxGCPauseMillis 設定的暫停時間限制,影響對響應時間敏感的應用程序的性能。
-XX:ParallelGCThreads=n
- 功能:
- 用于設置并行垃圾回收器的線程數量為 n。
- 在使用并行垃圾回收器(如 Parallel Scavenge 或 Parallel Old)時,該參數決定了在進行垃圾回收時同時運行的線程數量。
- 通常,n 的值可以根據 CPU 核心數來確定。如果不設置此參數,JVM 會根據實際的 CPU 核心數自動設置一個合適的值。在多核心的環境下,增加并行垃圾回收線程的數量可以提高垃圾回收的效率,但過多的線程也可能會帶來額外的線程切換開銷,所以要根據實際硬件和應用程序的特點來合理設置此參數。
3.響應時間優先(ParNew 收集器/CMS 收集器)
ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數、收集算法、回收策略等等)和 Serial 收集器完全一樣。
ParNew 收集器采用標記-復制算法

它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器配合工作。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項)的默認新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它。
自JDK 9開始還取消了ParNew加Serial Old以及Serial加CMS這兩組收集器組合的支持,并直接取消了-XX:+UseParNewGC參數,這意味著ParNew和CMS從此只能互相搭配使用,再也沒有其他收集器能夠和它們配合了。
讀者也可以理解為從此以后,ParNew合并入CMS,成為它專門處理新生代的組成部分。ParNew可以說是HotSpot虛擬機中第一款退出歷史舞臺的垃圾收集器。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
遺憾的是,CMS作為老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。
從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟(其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”):
- 初始標記: 短暫停頓,標記直接與 root 相連的對象(根對象);
- 并發標記: 同時開啟 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發生引用更新的地方。(并發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發運行;)
- 重新標記: 重新標記階段就是為了修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短
- 并發清除: 開啟用戶線程,同時 GC 線程開始對未標記的區域做清掃。(并發清除階段清理刪除掉標記階段判斷的已經死亡的對象,由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發的。)
總結
初始標記時耗費時間特別少,只是標記根對象。之后進行并發標記,此時用戶線程也在運行,所以可能會改變對象的引用,所以之后需要重新標記(也是stop-the-world),之后在并發清理。
在并發清理時用戶線程產生的垃圾成為浮動垃圾,只能在下次垃圾回收時清理。
注意:該垃圾回收器對CPU的占用并不高,但是此時用戶線程也會運行,又因為兩者同時運行,所以在垃圾回收時,會對程序的吞吐量有影響。

CMS主要優點:并發收集、低停頓。但是它有下面三個明顯的缺點:
- 對 CPU 資源敏感;
- 無法處理浮動垃圾;
- 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
CMS 垃圾回收器在 Java 9 中已經被標記為過時(deprecated),并在 Java 14 中被移除。
由于CMS收集器無法處理“浮動垃圾”(Floating Garbage),有可能出現“Con-current Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生。
在CMS的并發標記和并發清理階段,用戶線程是還在繼續運行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以后,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。
并行和并發概念補充:
并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。
并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個 CPU 上。
相關參數
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
UseConcMarkSweepGC:工作于老年代,基于標記清除的并發回收器,該垃圾回收器在運行時,其他用戶線程也可以同時運行,但是在某些階段還是需要stop-the-world。
UseParNewGC:工作于新生代,基于復制算法的垃圾回收器
SerialOld:UseConcMarkSweepGC回收器在有些情況下會出現并發失敗的問題,此時會采取補救的措施,老年代會從并發回收器退化到單線程回收器。
并發失敗的原因有:因為使用的基于標記清除的并發回收器,所以可能是因為內存碎片導致的,此時退化為單線程的垃圾回收器進行內存整理
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
ParallelGCThreads=n:并行線程數
ConcGCThreads=threads:并發GC線程數,一般設置為并行線程數的四分之一
-XX:CMSInitiatingOccupancyFraction=percent
在并發清理時用戶線程產生的垃圾成為浮動垃圾,只能在下次垃圾回收時清理。如設置這個參數為占用老年代內存80%時會觸發垃圾回收,預留一些空間存放浮動垃圾
-XX:+CMSScavengeBeforeRemark
在重新標記之前對新生代進行一次垃圾回收
4.G1(Garbage First收集器)
前言
在G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC)。
而G1跳出了這個樊籠,它可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬于哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。G1開創的基于Region的堆內存布局是它能夠實現這個目標的關鍵。
雖然G1也仍是遵循分代收集理論設計的,但其堆內存的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。
收集器能夠對扮演不同角色的Region采用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。
Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為只要大小超過了一個Region容量一半的對象即可判定為大對象。
每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪。而對于那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待。
介紹
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征。
被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征。它具備以下特點:
- 并行與并發:G1 能充分利用 CPU、多核環境下的硬件優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過并發的方式讓 java 程序繼續執行。
- 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
- 空間整合:與 CMS 的“標記-清除”算法不同,G1 從整體來看是基于“標記-整理”算法實現的收集器;從局部上來看是基于“標記-復制”算法實現的。
- 可預測的停頓:這是 G1 相對于 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒。
G1 收集器的運作大致分為以下幾個步驟:
- 初始標記: 短暫停頓(Stop-The-World,STW),標記從 GC Roots 可直接引用的對象,即標記所有直接可達的活躍對象
- 并發標記:與應用并發運行,標記所有可達對象。 這一階段可能持續較長時間,取決于堆的大小和對象的數量。
- 最終標記: 短暫停頓(STW),處理并發標記階段結束后殘留的少量未處理的引用變更。
- 篩選回收:根據標記結果,選擇回收價值高的區域,復制存活對象到新區域,回收舊區域內存。這一階段包含一個或多個停頓(STW),具體取決于回收的復雜度。

G1 收集器在后臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來) 。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)。
從 JDK9 開始,G1 垃圾收集器成為了默認的垃圾收集器。
G1 VS CMS
目前在小內存應用上CMS的表現大概率仍然要會優于G1,而在大內存應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間。當然,以上這些也僅是經驗之談,不同應用需要量體裁衣地實際測試才能得出最合適的結論,隨著HotSpot的開發者對G1的不斷優化,也會讓對比結果繼續向G1傾斜。

1.Young Collection
- 會 STW
G1會把堆內存劃分為一個個大小相等的區域,每個區域都可以獨立作為伊甸園、幸存區、老年代。

以上白色區域表示空閑區域,E則代表伊甸園區域,新創建的對象放在其中。
當伊甸園逐漸被占滿,就會觸發一次新生代的垃圾回收

垃圾回收會通過復制算法將幸存對象放入幸存區S

當幸存區中對象也比較多時,會觸發新生代垃圾回收,將一些對象晉升(老年代)O區域中,不滿足晉升條件的會將對象復制到其他的幸存區中。
2.Young Collection + CM
- 在 Young GC 時會進行 GC Root (根對象)的初始標記
- 老年代占用堆空間比例達到閾值時,進行并發標記(不會 STW),由下面的 JVM 參數決定
-XX:InitiatingHeapOccupancyPercent=percent (默認45%)

3.Mixed Collection
會對 E、S、O 進行全面垃圾回收
- 最終標記(Remark)會 STW
- 拷貝存活(Evacuation)會 STW
-XX:MaxGCPauseMillis=ms

- 伊甸園S中的幸存對象會被復制算法復制到幸存區中,幸存區S中不夠年齡的對象也會被復制到其他幸存區中,符合晉升條件的對象會晉升到老年代O中。
- 老年代垃圾回收時也采用的復制算法,將幸存對象復制到新的O中,老年代因為需要滿足最大暫停時間MaxGCPauseMillis,所以會有選擇的進行回收。
- 因為有時候堆內存太大了,導致老年代的回收時間比較長。所以G1會從老年代中選擇回收價值最高(垃圾最多的,可以釋放更多的空間)的區域,復制的區域少了,時間自然就變短了。
- 如果回收全部老年代也滿足最大暫停時間,那么會回收所有老年代區域。
4.Full GC
當G1老年代內存不足時,老年代占用堆空間比例達到閾值(默認45%)時,進行并發標記。后續進行混合收集的階段。
G1Full GC的時機:當垃圾回收的速度跟不上垃圾產生的速度時(并發回收失敗)。此時會退化為串行回收階段
5.Remark(重新標記階段)
- remark階段就是為了防止出現被引用的被當成垃圾回收,沒有被引用的沒有被垃圾回收的情況。
- 當對象的引用發生改變時,JVM會加入一個寫屏障。如B對象被A對象引用,則會在引用上加上寫屏障。
- 當觸發寫屏障后,會將B對象加入一個隊列中,并變為灰色表示沒有處理完。等到并發標記結束后,進入重新標記階段,重新標記階段會SSW,暫停其他用戶線程。將隊列中的對象進行重新標記。隊列的名稱叫satb_mark_queue

以上圖為并發標記階段,對象的處理狀態。 - 黑色表示處理完成,并且被引用。所以表示垃圾回收后會被保留下來的對象。
- 灰色表示處理當中的。
- 白色表示尚未處理的。
- 灰色、白色如果被引用,則最終會變成黑色。如果沒有被引用則變為白色(并發標記后)被當成垃圾回收。
ZGC 收集器
與 CMS 中的 ParNew 和 G1 類似,ZGC 也采用標記-復制算法,不過 ZGC 對該算法做了重大改進。
ZGC 可以將暫停時間控制在幾毫秒以內,且暫停時間不受堆內存大小的影響,出現 Stop The World 的情況會更少,但代價是犧牲了一些吞吐量。ZGC 最大支持 16TB 的堆內存。
ZGC 在 Java11 中引入,處于試驗階段。經過多個版本的迭代,不斷的完善和修復問題,ZGC 在 Java15 已經可以正式使用了。
不過,默認的垃圾回收器依然是 G1。你可以通過下面的參數啟用 ZGC:
java -XX:+UseZGC className
java -XX:+UseZGC className
在 Java21 中,引入了分代 ZGC,暫停時間可以縮短到 1 毫秒以內。
你可以通過下面的參數啟用分代 ZGC:
java -XX:+UseZGC -XX:+ZGenerational className
相關文章:
JVM內存結構
- JVM內存結構筆記01-運行時數據區域
- JVM內存結構筆記02-堆
- JVM內存結構筆記03-方法區
- JVM內存結構筆記04-字符串常量池
- JVM內存結構筆記05-直接內存
- JVM內存結構筆記06-HotSpot虛擬機對象探秘
- JVM中常量池和運行時常量池、字符串常量池三者之間的關系
JVM垃圾回收
- JVM垃圾回收筆記01-垃圾回收算法
- JVM垃圾回收筆記02-垃圾回收器