前面文章中,我們介紹了 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 回收器,垃圾回收器不斷改進,使得垃圾回收效率不斷提升。特別是分區思想誕生后,對於垃圾回收停頓時間的控制更加細膩,可以讓應用有更完美的延時控制,從而呈現更好的用戶體驗。
參考資料
如果只是看,其實無法真正學會知識的。為了幫助大家更好地學習,我建了一個虛擬機群,專門討論學習 Java 虛擬機方面的內容,每周針對我所發文章進行討論答疑。如果你有興趣,關注「Java技術精選」公眾號,通過右下角菜單「入群交流」加我好友,小助手會拉你入群。
- 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垃圾回收器
- JVM基礎系列第10講:垃圾回收的幾種類型
- JVM基礎系列第11講:JVM參數之堆棧空間配置
- JVM基礎系列第12講:JVM參數之查看JVM參數
- JVM基礎系列第13講:JVM參數之追蹤類信息
- JVM基礎系列第14講:JVM參數之GC日志配置
- JVM基礎系列第15講:JDK性能監控命令