JVM系列第9講:JVM垃圾回收器
前面文章中,我們介紹了 Java 虛擬機器的記憶體結構,Java 虛擬機器的垃圾回收機制,那麼這篇文章我們說說具體執行垃圾回收的垃圾回收器。
總的來說,Java 虛擬機器的垃圾回收器可以分為四大類別:序列回收器、並行回收器、CMS 回收器、G1 回收器。
序列回收器
序列回收器是指使用單執行緒進行垃圾回收的回收器。因為每次回收時只有一個執行緒,因此序列回收器在併發能力較弱的計算機上,其專注性和獨佔性的特點往往能讓其有更好的效能表現。
序列回收器可以在新生代和老年代使用,根據作用於不同的堆空間,分為新生代序列回收器和老年代序列回收器。
新生代序列回收器
序列收集器是所有垃圾回收器中最古老的一種,也是 JDK 中最基本的垃圾回收器之一。
在新生代序列回收器中使用的是複製演算法。在序列回收器進行垃圾回收時,會觸發 Stop-The-World 現象,即其他執行緒都需要暫停,等待垃圾回收完成。因此在某些情況下,其會造成較為糟糕的使用者體驗。
使用 -XX:+UseSerialGC
引數可以指定使用新生代序列收集器和老年代序列收集器。當虛擬機器在 Client 模式下執行時,其預設使用該垃圾收集器。
老年代序列回收器
在老年代序列回收器中使用的是標記壓縮演算法。其與新生代序列收集器一樣,只能序列、獨佔式地進行垃圾回收,因此也經常會有較長時間的 Stop-The-World 發生。
但老年代序列回收器的好處之一,就是其可以與多種新生代回收器配合使用。若要啟用老年代序列回收器,可以嘗試以下引數:
-XX:UseSerialGC
:新生代、老年代都使用序列回收器。-XX:UseParNewGC
:新生代使用 ParNew 回收器,老年代使用序列回收器。-XX:UseParallelGC
:新生代使用 ParallelGC 回收器,老年代使用序列回收器。
並行回收器
並行回收器在序列回收器的基礎上做了改進,其使用多執行緒進行垃圾回收。對於並行能力強的機器,可以有效縮短垃圾回收所使用的時間。
根據作用記憶體區域的不同,並行回收器也有三個不同的回收器:新生代 ParNew 回收器、新生代 ParallelGC 回收器、老年代 ParallelGC 回收器。
新生代 ParNew 回收器
新生代 ParNew 回收器工作在新生代,其只是簡單地將序列回收器多執行緒化,其回收策略、演算法以及引數和新生代序列回收器一樣。
新生代 ParNew 回收器同樣使用複製的垃圾回收演算法,其垃圾收集過程中同樣會觸發 Stop-The-World 現象。但因為其使用多執行緒進行垃圾回收,因此在併發能力強的 CPU 上,其產生的停頓時間要短於序列回收器。
但在單 CPU 或並能能力弱的系統中,並行回收器效果會因為執行緒切換的原因,其實際表現反而不如序列回收器。
要開啟新生代 ParNew 回收器,可以使用以下引數:
-XX:+UseParNewGC
:新生代使用 ParNew 回收器,老年代使用序列回收器。-XX:UseConcMarkSweepGC
:新生代使用 ParNew 回收器,老年代使用 CMS。-XX:ParallelGCThreads
:指定 ParNew 回收器的工作執行緒數量。
新生代 Parallel GC 回收器
新生代 Parallel GC 回收器與新生代 ParNew 回收器非常類似,其也是使用複製演算法,都是多執行緒、獨佔式的收集器,也會導致 Stop-The-World。但其餘 ParNew 回收器的一個重大不同是:其非常注重系統的吞吐量。
之所以說新生代 Parallel GC 回收器非常注重系統吞吐量,是因為其有一個自適應 GC 調節策略。我們可以使用 -XX:+UseAdaptiveSizePolicy
引數開啟這個策略,在這個模式下,新生代的大小、Eden 和 Survivor 的比例、晉升老年代的物件年齡等引數都會被自動調節,已達到堆大小、吞吐量、停頓時間的平衡點。
Parallel GC 回收器提供了兩個重要引數用於控制系統的吞吐量。
-XX:MaxGCPauseMillis
:設定最大垃圾收集停頓時間。在 ParallelGC 工作時,其會自動調整響應引數,將停頓時間控制在設定範圍內。為了達到目的,其可能會使用較小的堆,但這會導致 GC 較為頻繁。-XX:GCTimeRatio
:設定吞吐量大小,其實一個 0 - 100 的整數。假設 GCTimeRatio 的值為 n,那麼系統將不花費超過 1/(1+n) 的時間用於垃圾手機。比如 GCTimeRatio 值為 19,那麼系統用於垃圾收集的時間不超過 1 /(1+19) = 5%。預設情況下,它的取值是 99,即不超過 1% 的時間用於垃圾收集。
新生代 Parallel GC 回收器可以使用以下引數啟用:
-XX:+UseParallelGC
:新生代使用 Parallel 回收器,老年代使用序列回收器。-XX:+UseParallelOldGC
:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
老年代 ParallelOldGC 回收器
老年代 ParallelOldGC 回收器也是一種多執行緒併發的回收器,與新生代 ParallelGC 收集器一樣,其也是注重吞吐量的收集器,只不過其是作用於老年代。
ParallelOldGC 回收器使用的是標記壓縮演算法,只有在 JDK 1.6 中才可以使用。我們可以使用-XX:UseParallelOldGC
引數在新生代中使用 ParallelGC 收集器,在老年代中使用 ParallelOldGC 收集器。引數 -XX:ParallelGCThreads
也可以用於設定垃圾回收時的執行緒數量。
CMS 回收器
與 ParallelGC 和 ParallelOldGC 不同,CMS 回收器主要關注系統停頓時間。CMS 回收器全稱為 Concurrent Mark Sweep,意為標記清除演算法,其是一個使用多執行緒並行回收的垃圾回收器。
工作步驟
CMS 的主要工作步驟有:初始標記、併發標記、預清理、重新標記、併發清除和併發充值。其中初始標記和重新標記是獨佔系統資源的,而其他階段則可以和使用者執行緒一起執行。
在整個 CMS 回收過程中,預設情況下會有預清理的操作,我們可以關閉開關 -XX:-CMSPrecleaningEnabled
不進行預清理。因為重新標記是獨佔 CPU 的,因此如果新生代 GC 發生之後,立刻出發一次新生代 GC,那麼停頓時間就會很長。為了避免這種情況,預處理時會刻意等待一次新生代 GC 的發生,之後在進行預處理。
主要引數
啟動 CMS 回收器刻意使用引數:-XX:+UseConcMarkSweepGC
,執行緒併發數量刻意通過 -XX:ConcGCThreads
或 -XX:ParallelCMSThreads
引數設定。
此外,我們還可以設定 -XX:CMSInitiatingOccupancyFraction
來指定老年代空間使用閾值。當老年代空間使用率達到這個閾值時,會執行一次 CMS 回收,而不像其他回收器一樣等到記憶體不夠用的時候才進行 GC。
我們之前說過標記清除演算法的缺點是會產生記憶體碎片,因此 CMS 回收器會產生較多記憶體碎片。我們可以使用 XX:+UseCMSCompactAtFullCollection
引數讓 CMS 在完成垃圾回收後,進行一次記憶體碎片整理。使用 -XX:CMSFullGCsBeforeCompaction
引數設定進行多少次 CMS 回收後,進行一次記憶體壓縮。
此外,如果希望使用 CMS 回收 Perm 區,那麼則可以開啟 -XX:+CMSClassUnloadingEnabled
開關。開啟該開關後,如果條件允許,那麼系統會使用 CMS 的機制回收 Perm 區 Class 資料。
G1 回收器
G1 回收器是 JDK 1.7 中使用的全新垃圾回收器,從長期目標來看,其是為了取代 CMS 回收器。
G1 回收器擁有獨特的垃圾回收策略,和之前所有垃圾回收器採用的垃圾回收策略不同。從分代看,G1 依然屬於分代垃圾回收器。但它最大的改變是使用了分割槽演算法,從而使得 Eden 區、From 區、Survivor 區和老年代等各塊記憶體不必連續。
在 G1 回收器之前,所有的垃圾回收器其記憶體分配都是連續的一塊記憶體,如下圖所示。
而在 G1 回收器中,其將一大塊的記憶體分為許多細小的區塊,從而不要求記憶體是連續的。
從上圖可以看到,每個Region被標記了 E、S、O 和 H,說明每個 Region 在執行時都充當了一種角色。所有標記為 E 的都是 Eden 區的記憶體,它們散落在記憶體的各個角落,並不要求記憶體連續。同理,Survivor 區、老年代(Old)也是如此。
從上圖我們還可以看到 H 是以往演算法中沒有的,它代表 Humongous。這表示這些 Region 儲存的是巨型物件(humongous object,H-obj),當新建物件大小超過 Region 大小一半時,直接在新的一個或多個連續 Region 中分配,並標記為 H。
堆記憶體中一個 Region 的大小可以通過 -XX:G1HeapRegionSize
引數指定,大小區間只能是1M、2M、4M、8M、16M 和 32M,總之是2的冪次方。如果G1HeapRegionSize 為預設值,即把設定的最小堆記憶體按照2048份均分,最後得到一個合理的大小。
工作步驟
G1 收集器的收集過程主要有四個階段:
- 新生代 GC
- 併發標記週期
- 混合收集
- 如果需要,可能進行 FullGC
新生代 GC 與其他垃圾收集器的類似,就是清空 Eden 區,將存活物件移動到 Survivor 區,部分年齡到了就移動到老年代。
併發標記週期則分為:初始標記、根區域掃描、併發標記、重新標記、獨佔清理、併發清理階段。其中初始標記、重新標記、獨佔清理是獨佔式的,會引起停頓。並且初始標記會引發一次新生代 GC。在這個階段,所有將要被回收的區域會被 G1 記錄在一個稱之為 Collection Set 的集合中。
混合回收階段會首先針對 Collection Set 中的記憶體進行回收,因為這些垃圾比例較高。G1 回收器的名字 Garbage First 就是這個意思,垃圾優先處理的意思。在混合回收的時候,也會執行多次新生代 GC 和 混合 GC,從而來進行記憶體的回收。
必要時進行 Full GC。當在回收階段遇到記憶體不足時,G1 會停止垃圾回收並進行一次 Full GC,從而騰出更多空間進行垃圾回收。
相關引數
開啟 G1 收集器,我們可以使用引數:`-XX:+UseG1GC。
設定目標最大停頓時間,可以使用引數:-XX:MaxGCPauseMillis
。
設定 GC 工作執行緒數量,可以使用引數:-XX:ParallelGCThreads
。
設定堆使用率觸發併發標記週期的執行,可以使用引數:-XX:InitiatingHeapOccupancyPercent
。
總結
從一開始的序列回收器,到後來的並行回收器、CMS回收器,到最後的 G1 回收器,垃圾回收器不斷改進,使得垃圾回收效率不斷提升。特別是分割槽思想誕生後,對於垃圾回收停頓時間的控制更加細膩,可以讓應用有更完美的延時控制,從而呈現更好的使用者體驗。
參考資料
JVM系列目錄
- JVM系列開篇:為什麼要學虛擬機器?
- JVM系列第1講:Java 語言的前世今生
- JVM系列第2講:Java 虛擬機器的歷史
- JVM系列第3講:到底什麼是虛擬機器?
- JVM系列第4講:從原始碼到機器碼,發生了什麼?
- JVM系列第5講:位元組碼檔案結構
- JVM系列第6講:Java 虛擬機器記憶體結構
- JVM系列第7講:JVM 類載入機制
- JVM系列第8講:JVM 垃圾回收機制
- JVM系列第9講:JVM垃圾回收器
如果只是看,其實無法真正學會知識的。為了幫助大家更好地學習,我建了一個虛擬機器群,專門討論學習 Java 虛擬機器方面的內容,每週針對我所發文章進行討論答疑。如果你有興趣,關注「Java技術精選」公眾號,通過右下角選單「入群交流」加我好友,小助手會拉你入群。