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

大家好,我是樹哥。

前段時間有個小夥伴去面試,被問到了 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」。 在這個階段,垃圾回收線程與用戶線程可以併發執行,因此並不影響用戶的響應時間。

引用自《深入理解Java虛擬機》

從上面的描述步驟中我們可以看出: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 停頓時間,最後再通過「重新標記」解決了併發執行產生的問題。

參考資料

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