關於 CMS 垃圾回收器,你真的懂了嗎?

2022-07-27 12:01:31

大家好,我是樹哥。

前段時間有個小夥伴去面試,被問到了 CMS 垃圾回收器的詳細內容,沒答出來。實際上,CMS 垃圾回收器是回收器歷史上很重要的一個節點,其開啟了 GC 回收器關注 GC 停頓時間的歷史。今天,就讓樹哥帶你一起來學一波吧!

CMS 回收器的歷史

如果你是一個比較資深的 Java 開發者,那你或許會對 CMS 垃圾回收器嗤之以鼻,然後說一句:CMS 垃圾回收器早就過時了,現在都流行 G1、ZGC 垃圾回收器了!學這個東西一點用都沒有!

確實如資深開發者所說,現在 CMS 垃圾回收器是比較過時的設定了。CMS 垃圾回收器於 JDK1.5 時期推出,在 JDK9 中被廢棄,在 JDK14 中被移除。 而用來替換 CMS 垃圾回收器的便是我們常說的 G1 垃圾回收器。

但 G1 垃圾回收器也是在 CMS 的基礎上進行改進的,因此簡單瞭解下 CMS 垃圾回收器也是有必要的。

CMS 回收器簡介

CMS(Concurrent Mark Sweep)垃圾回收器是第一個關注 GC 停頓時間的垃圾收集器。 在這之前的垃圾回收器,要麼就是序列垃圾回收方式,要麼就是關注系統吞吐量。這樣的垃圾回收器對於強互動的程式很不友好,而 CMS 垃圾回收器的出現,則打破了這個尷尬的局面。因此,CMS 垃圾回收器誕生之後就受到了大家的歡迎,導致現在還有非常多的應用還在繼續使用它。

CMS 垃圾回收器之所以能夠實現對 GC 停頓時間的控制,其本質來源於對「根可達演演算法」的改進,即三色標記演演算法。在 CMS 垃圾回收器出現之前,無論是 Serious 垃圾回收器,還是 ParNew 垃圾回收器,亦或是 Parallel Scavenge 垃圾回收器,他們在進行垃圾回收的時候都需要 Stop the World,即無法實現垃圾回收執行緒與使用者執行緒並行執行。而 CMS 垃圾回收器通過三色標記演演算法,實現了垃圾回收執行緒與使用者執行緒並行執行,從而極大地降低了系統響應時間,提高了強互動應用程式的體驗。

對於 CMS 垃圾回收器來說,其實通過「標記-清除」演演算法實現的,它的執行過程分為 4 個步驟,包括:

  • 初始標記
  • 並行標記
  • 重新標記
  • 並行清除

初始標記,指的是尋找所有被 GCRoots 參照的物件,該階段需要「Stop the World」。 這個步驟僅僅只是標記一下 GC Roots 能直接關聯到的物件,並不需要做整個參照的掃描,因此速度很快。

並行標記,指的是對「初始標記階段」標記的物件進行整個參照鏈的掃描,該階段不需要「Stop the World」。 對整個參照鏈做掃描需要花費非常多的時間,因此通過垃圾回收執行緒與使用者執行緒並行執行,可以降低垃圾回收的時間,從而降低系統響應時間。這也是 CMS 垃圾回收器能極大降低 GC 停頓時間的核心原因,但這也帶來了一些問題,即:並行標記的時候,參照可能發生變化,因此可能發生漏標(本應該回收的垃圾沒有被回收)和多標(本不應該回收的垃圾被回收)了。

重新標記,指的是對「並行標記」階段出現的問題進行校正,該階段需要「Stop the World」。 正如並行標記階段說到的,由於垃圾回收演演算法和使用者執行緒並行執行,雖然能降低響應時間,但是會發生漏標和多標的問題。所以對於 CMS 回收器來說,它需要這個階段來做一些校驗,解決並行標記階段發生的問題。

並行清除,指的是將標記為垃圾的物件進行清除,該階段不需要「Stop the World」。 在這個階段,垃圾回收執行緒與使用者執行緒可以並行執行,因此並不影響使用者的響應時間。

從上面的描述步驟中我們可以看出:CMS 之所以能極大地降低 GC 停頓時間,本質上是將原本冗長的參照鏈掃描進行切分。通過 GC 執行緒與使用者執行緒並行執行,加上重新標記校正的方式,減少了垃圾回收的時間。

CMS 回收器優缺點

從上面的描述我們可以知道,CMS 回收器的優點是:並行收集垃圾、低停頓。但其也有下面幾個明顯的缺點:

對 CPU 資源消耗較大。 CMS 回收器在並行標記和並行清理階段,是需要啟用多個執行緒進行處理的,這就意味著它需要佔用一部分執行緒資源,即 CPU 資源。預設情況下 CMS 啟用的垃圾回收執行緒數是(CPU數量 + 3)/4,當 CPU 數量越大時,啟用的垃圾回收執行緒數佔比就越小。

但如果 CPU 數量越小,例如只有 2 個 CPU 時,垃圾回收執行緒佔用就達到了 50%,也就是說需要拿 50% 的 CPU 時間來進行垃圾回收。這就會極大地降低系統的吞吐量,這是讓人無法接受的情況。

無法處理浮動垃圾。 由於 CMS 並行標記階段會發生漏標的情況,因此會有一些本該回收的垃圾物件無法被回收。此外,在 CMS 進行並行清理的時候,使用者執行緒同時在執行,也會產生一些浮動垃圾。因此對於 CMS 回收器來說,其需要留出一些空間給這些浮動垃圾儲存。

在 JDK1.5 的預設設定中,當老年代空間已用空間大於 68% 之後,CMS 垃圾回收器便會開始進行垃圾清理。這個數值相對比較保守一些,我們可以通過 -XX:CMSInitiatingOccupancyFraction 引數自行調節。在 JDK1.6 種,該閾值被提升至 92%。

如果在 CMS 執行期間發現預留的記憶體無法滿足程式需要,就會提示「Concurrent Mode Failure」錯誤。此時虛擬機器器採用後備方案:臨時啟用 Serial Old 回收器來重新進行老年代的垃圾回收,這時候 Stop the World 的時間可能就會很長了。

產生空間碎片。 由於 CMS 是基於「標記-清除」演演算法實現的回收器,因此其會產生很多空間碎片,這會導致給大物件分配的時候很麻煩,會提前觸發 Full GC。為了解決這個問題,CMS 回收器提供了 -XX:+UseCMSCompactAtFullCollection 引數來解決這個問題,意思是在空間不夠的時候進行空間整理,這個引數預設是開啟的。

該引數通常和 -XX:CMSFullGCsBeforeCompaction 一起使用,後者用於設定執行多少次不壓縮的 Full GC 之後,跟著來一次帶壓縮的 Full GC(預設值是 0,表示每次進入 Full GC 時都進行碎片整理)。

總結

CMS 回收器,誕生於 JDK1.5,失落於 JDK9,卒於 JDK14。它的誕生,開啟了垃圾回收器專注於優化 GC 停頓時間的歷史,隨後的 G1、ZGC 都在 CMS 的基礎之上改進、優化而來。

而 CMS 回收器之所以能實現對 GC 停頓時間的強力控制,全都歸功於對於「根可達演演算法」的優化。其將序列的參照鏈掃描,拆分成了「初始標記」和「並行標記」兩個階段,從而極大地降低了 GC 停頓時間,最後再通過「重新標記」解決了並行執行產生的問題。

參考資料