Java 垃圾收集之垃圾收集器介紹

垃圾收集器

並行與併發

垃圾收集器有並行、併發的,解釋一下這兩個名詞:

  • 並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。
  • 併發(Concurrent):指用戶線程與垃圾收集器線程同時執行(但並不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行於另一個 CPU 上。

垃圾收集器是內存回收的具體實現,HotSpot 虛擬機包含的收集器如圖:
在這裏插入圖片描述
上圖中有7種作用於不同分代的收集器,如果兩個收集器之間存在連線,說明它們可以搭配使用,其中 Young generation 代表新生代,Tenured generation 代表老年代。下面介紹一下這幾種收集器。

Serial 收集器

Serial 收集器是最基本,歷史最悠久的收集器。這個收集器是一個單線程的收集器,“單線程”的意義不只是說明它只會使用一個 CPU 或一條執行線程去完成垃圾收集工作,更重要的是在進行垃圾收集時必須暫停其它所有工作線程(Stop The World),直到它收集結束。這項工作實際上是由虛擬機在後臺自動發起和自動完成的。Serial / Serial Old 收集器的運行過程如下:
在這裏插入圖片描述
雖然 Serial 收集器在進行回收時會暫停用戶線程,但是它也有優於其它收集器的地方:簡單高效(與其它收集器的單線程比),對於限定單個 CPU 的環境來說,Serial 收集器由於沒有線程交互的開銷,自然可以獲得最高的單線程收集效率。Serial 收集器對運行在 Client 模式下的虛擬機來說是一個很好的選擇。

ParNew 收集器

ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多條線程進行垃圾收集外,其餘行爲與 Serial 收集器完全一樣。ParNew 收集器的運行過程如下:
在這裏插入圖片描述
ParNew 收集器是許多運行在 Server 模式下的虛擬機種首選的新生代收集器,其中一個與性能無關的很重要的原因是,除了 Serial 收集器外,目前只有它能與 CMS 收集器(Concurrent Mark Sweep)配合工作。這款收集器是 HotSpot 虛擬機中第一款真正意義上的併發(Concurrent)處理器,它第一次實現了垃圾收集線程與用戶線程同時工作。ParNew 收集器是使用 -XX:+UseConcMarkSweepGC 選項後的默認新生代收集器,也可以使用 -XX:+UseParNewGC 選項來強制指定它。
ParNew 收集器在單線程環境下沒有 Serial 收集器效率高,隨着 CPU 的數量增加,它對於 GC 時系統資源的有效利用還是很有好處的。它默認開啓的收集線程數與 CPU 的數量相同,可以使用 -XX:ParallelGCThreads 參數來限制垃圾收集器的線程數。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一個新生代收集器,它也是用複製算法的收集器,又是並行的多線程收集器,和 ParNew 相比,它有什麼特別之處呢?
Parallel Scavenge 收集器的特點是它的關注點與其它收集器不同,CMS 等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是 CPU 運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。
高吞吐量可以高效利用 CPU 時間,儘快完成程序的運算任務,適合在後臺運算而不需要太多交互的任務。
Parallel Scavenge 收集器提供了兩個參數控制吞吐量,分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis 參數以及直接設置吞吐量大小的 -XX:GCTimeRatio 參數。此外,Parallel Scavenge 收集器還有一個參數 -XX:+UseAdaptiveSizePolicy,這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden 與 Survivor 區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大吞吐量,這種調節方式稱爲 GC 自適應的調節策略(GC Ergonomics)。

Serial Old 收集器

Serial Old 收集器是 Serial 收集器的老年代版本,同樣是一個單線程收集器,使用“標記-整理”算法。主要是給 Client 模式下的虛擬機使用。在 Server 模式下主要有兩大用途:一種是在 JDK 1.5 以及之前的版本與 Parallel Scavenge 收集器搭配使用,另一種用途就是作爲 CMS 收集器的後備方案,在併發收集發生 Concurrent Mode Failure 時使用。Serial Old 收集器工作過程同 Serial 收集器。

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和“標記-整理”算法。
在注重吞吐量以及 CPU 資源敏感的場合,可以優先考慮 Parallel Scavenge + Parallel Old 收集器組合。Parallel Old 收集器工作過程如下:
在這裏插入圖片描述

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以收穫最短回收停頓時間爲目標的收集器,非常符合重視服務的響應速度,希望系統停頓時間最短這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基於“標記-清除”算法實現的,整個過程分爲4步:

  • 初始標記(CMS initial mark)
  • 併發標記(CMS concurrent mark)
  • 重新標記(CMS remark)
  • 併發清除(CMS concurrent sweep)

其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,併發標記階段就是進行 GC Roots Tracing 的過程,而重新標記階段則是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動那一部分對象的標記記錄。
由於整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以總體上來說 CMS 收集器的內存回收過程是與用戶線程一起併發執行的。CMS 收集器運行過程如下:
在這裏插入圖片描述
CMS 收集器是一款優秀的收集器:併發收集、低停頓。但是也存在3個明顯的缺點:

  • CMS 收集器對 CPU 資源非常敏感,工作時會因爲佔用 CPU 資源導致應用程序緩慢,總吞吐量降低。CMS 默認啓動的線程數是(CPU 數量 + 3)/ 4,也就是 CPU 在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且伴隨着 CPU 數量的增加而下降。但是當 CPU 數量不足4個時,CMS 對用戶程序的影響就可能變得很大。
  • CMS 收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure” 失敗而導致另一次 Full GC 的產生。由於 CMS 併發清理階段用戶線程還在運行着,這樣會有新垃圾不斷產生,這部分垃圾出現在標記過程之後,CMS 無法在當此收集中處理掉他們,只好等下一次 GC 時再清理掉。這部分垃圾就是“浮動垃圾”。另外,由於垃圾收集階段用戶線程還需要執行,所以還需要預留足夠空間給用戶線程使用,因此 CMS 收集器需要預留一部分空間提供併發收集時的程序運作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 來指定老年代使用多少空間(百分比)後被激活。如果 CMS 運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備方案:臨時啓用 Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以參數設置太高容易導致大量“Concurrent Mode Failure”失敗,性能反而降低。
  • 由於 CMS 是一款基於“標記-清除”算法實現的收集器,所以收集結束後會產生大量空間碎片,當有大對象分配時連續空間不足而觸發一次 Full GC。CMS 收集器提供了一個 -XX:+UseCMSCompactAtFullCollection 開關參數(默認開啓),用於在 CMS 收集器頂不住要進行 Full GC 時開啓內存碎片的合併整理過程。

G1 收集器

G1(Garbage-First)收集器是一款面向服務端應用的垃圾收集器,與其他 GC 收集器相比,G1 具備如下特點:

  • 並行與併發:G1 能充分利用多 CPU、多核環境下的硬件優勢,使用多個 CPU 來縮短 Stop-The-World 停頓的時間,部分其它收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過併發的方式讓 Java 程序繼續執行。
  • 分代收集:雖然 G1 可以不需要與其它收集器配合就能獨立管理整個 GC 堆,但它能夠採用不同方式去處理新創建的對象和已存活了一段時間、熬過多次 GC 的舊對象以獲取更好的收集效果。
  • 空間整合:與 CMS 的“標記-清除”算法不同,G1 從整體來看是基於“標記-整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的。無論如何都不會產生內存空間碎片。
  • 可預測的停頓:這是 G1 相對於 CMS 來說的另一大優勢,G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒,這幾乎已經是實時 Java(RTSJ)的垃圾收集器的特徵了。

在 G1 之前的其它收集器進行收集的範圍都是整個新生代或者老年代,而 G1 不再是這樣。使用 G1 收集時,Java 堆的內存佈局就與其它收集器有很大差別,它將整個 Java 堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,他們都是一部分 Region(不需要連續)的集合。
G1 之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個 Java 堆中進行全區域的垃圾收集。G1 跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收可獲得空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region(這也是 Garbage-First 名稱的來由)。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限的時間內可以獲取儘可能高的收集效率。
在 G1 中,Region 之間的對象引用以及其它收集器中的新生代與老年代之間的對象引用,虛擬機都是使用 Remembered Set 來避免全堆掃描的。G1 中每個 Region 都有一個與之對應的 Remembered Set,虛擬機發現程序在對 Reference 類型的數據進行寫操作時,會產生一個 Write Barrier 暫時中斷寫操作,檢查 Reference 引用的對象是否處於不同的 Region 之中(在分代中的例子就是檢查老年代中的對象是否引用了新生代中的對象) ,如果是,則通過 CardTable 把相關引用信息記錄到被引用對象所屬的 Region 的 Remembered Set 之中。當進行內存回收時,在 GC 根節點的枚舉範圍中加入 Remembered Set 即可保證不對全堆掃描也不會有遺漏。
如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可分爲以下幾個步驟:

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

G1 的前幾個步驟運作過程和 CMS 有很多相似之處。
初始標記僅僅標記一下 GC Roots 能直接關聯到的對象,並修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發執行時,能在正確可用的 Region 中創建新對象,這階段需要線程停頓,但耗時很短。
併發標記階段是從 GC Root 開始對堆中對象進行可達性分析,找出存活對象,這階段耗時較長,但可與用戶程序併發執行。
最終標記階段則是爲了修改在併發標記期間因用戶程序繼續運作而導致標記產生變化的那部分標記記錄(一個規律:併發執行的收集器一般都需要重新標記的過程),虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中,這階段需要停頓線程,但是可併發執行。
篩選回收階段首先對各個 Region 的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。
G1 收集器運行過程如下:
在這裏插入圖片描述

GC 日誌說明

有如下 GC 日誌:
[GC (System.gc()) [PSYoungGen: 3952K->808K(75776K)] 3952K->816K(249344K), 0.0103407 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(75776K)] [ParOldGen: 8K->710K(173568K)] 816K->710K(249344K), [Metaspace: 3420K->3420K(1056768K)], 0.0042775 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
日誌開頭的 “[GC” 和 “[Full GC” 說明了這次垃圾回收的停頓類型,“ [PSYoungGen”、“[ParOldGen” 代表 GC 發生的區域,方括號之內的 “3952K->808K(75776K)” 含義是 “GC 前該內存區域已使用的容量 -> GC後該內存區域已使用容量(該內存區域總容量)”。方括號之外的 “3952K->816K(249344K)” 表示 “GC 前 Java 堆已使用容量 -> GC 後 Java 堆已使用容量(Java 堆總容量)”。
“ [Times: user=0.00 sys=0.00, real=0.01 secs]” 中,user 代表用戶態消耗的 CPU 時間,sys 代表內核態消耗的 CPU時間,real 代表操作從開始到結束所經過的牆鍾事件(Wall Clock Time)。牆鍾事件也包括各種非運算的等待耗時,如等待磁盤 I/O、等待線程阻塞等,而 CPU 時間不包括這些耗時。

垃圾收集器參數總結

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章