關于 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 停頓時間,最后再通過「重新標記」解決了并發執行產生的問題。
參考資料
CMS 低延遲垃圾收集器詳解 - 掘金
深入理解 JAVA 垃圾收集器 CMS,G1 工作流程原理 - 掘金
深入理解 Java 虛擬機:JVM 高級特性與最佳實踐(第 2 版)- 周志明 - 微信讀書?