1. 程式人生 > 實用技巧 >探索ParNew和CMS垃圾回收器

探索ParNew和CMS垃圾回收器

前言

上篇文章我們一起分析了JVM的垃圾回收機制,瞭解了新生代的記憶體模型,老年代的空間分配擔保原則,並簡單的介紹了幾種垃圾回收器。詳細內容小夥伴們可以去看一下我的上篇文章:秒懂JVM的垃圾回收機制

今天我們就來探索一下,ParNew和CMS垃圾回收器的實現過程。

ParNew垃圾回收器

現在,如果沒有使用G1垃圾回收器,通常情況下大家都是用的ParNew作為新生代的垃圾回收器。

首先我們思考一個問題,假如我們的伺服器CPU是4核的,如果對新生代垃圾回收的時候,僅僅使用單執行緒進行,是不是就會導致CPU的效能無法發揮?

所以ParNew垃圾回收器主打的就是多執行緒的垃圾回收機制,老版本的Serial垃圾回收器主打的是單執行緒垃圾回收,他們都是對新生代進行垃圾回收的,唯一的區別就是單執行緒和多執行緒的區別,垃圾回收的演算法是一樣的,都是複製回收演算法,上篇文章已經詳細做過介紹,本篇文章不在重複介紹。

那麼如何指定垃圾回收器為ParNew呢?

很簡單,只要使用“-XX:+UseParNewGC”選項,只要加入這個選項,JVM啟動之後對新生代的垃圾回收就是使用的ParNew垃圾回收器了。

後邊的過程,垃圾回收演算法,以及升級到老年代條件就是上篇文章我們介紹的那樣。

預設情況下,如果指定為ParNew垃圾回收器,它會給自己設定與CPU核心數相同的垃圾回收執行緒。

如果要自定義垃圾回收執行緒數,可以使用“-XX:ParallelGCThreads”引數即可,但一般不建議修改此引數。

CMS垃圾回收器

老年代我們一般使用CMS進行垃圾回收。它採用的是標記清理演算法,其實也很簡單,就是先標記出哪些物件是垃圾物件,然後把這些物件清理掉。

通過上圖,我們會發現一個問題,這種演算法會造成很多記憶體碎片,這種碎片是大小不一的,可能放不下一個物件,那麼這塊記憶體就被浪費掉了。

也可能因為記憶體碎片太多,導致記憶體利用率很低,從而頻繁引發FULL GC。這就是CMS的一個缺點了。

那麼當發生FULL GC後,可能會先引發“Stop the World”,然後再採用標記清除演算法回收垃圾,這樣會有什麼問題?

之前我們介紹過,當發生“Stop the World”的時候,會停止一切工作執行緒,導致程式卡頓,所以CMS的垃圾回收方式其實不是這樣的。

CMS採取的是垃圾回收執行緒和系統工作執行緒儘量同時執行的模式來處理垃圾回收的。

一共分為四個階段:初始標記、併發標記、重新標記、併發清理。

我們一個一個來看。

首先CMS進行Full GC了,會先執行初始標記階段,這個階段會引發“Stop the World”狀態,停止所有工作執行緒,然後標記出所有GC Roots直接引用的物件。

public class Main {
    private static SysUser1 sysUser1 = new SysUser1();
}
public class Main {
    private  SysUser2 sysUser2 = new SysUser2();
}

比如上邊的程式碼,在這一階段僅僅會標記出靜態變數sysUser1這個物件,而不會去管sysUser2物件,因為它是例項變數引用的。

方法的區域性變數和類的靜態變數是GC Roots,但是類的例項變數不是GC Roots。

所以第一個階段雖然會造成“Stop the World”,但是實際影響不大,因為僅僅標記了GC Roots直接引用的物件,不會耗時太久。、

接下來進入第二階段,併發標記階段,這個階段系統程序可以隨意建立新的物件,正常執行。

在這一階段中,可能有新的物件建立,也可能有舊的物件變成垃圾物件,CMS會盡可能對已有物件進行GC Roots追蹤,看看類似sysUser2這種物件被誰引用了。

如果它被間接的引用了,那麼此時就不需要回收它。

簡單的理解,第二階段就是對老年代所以物件進行GC Roots追蹤,這個還是很耗費時間的,但由於沒有停止系統工作執行緒,所以不會對系統產生影響。

接著進入第三階段,重新標記階段。

因為第二階段系統正常執行,所以結束後一定還會存在新的存活物件和垃圾物件是未被標記的。

所以在第三階段將會再次觸發“Stop the World”狀態,停止系統工作執行緒。

然後重新標記在第二階段中新建立的物件和新成為垃圾的物件。

這一過程是很快的,因為要標記的物件其實是很少的。

最後重新恢復系統工作程序,進入第四階段:併發清理階段。

這一階段系統正常執行,然後CMS會對之前已經標記過的物件進行垃圾清理。

這一階段也是很耗時的,但系統還在正常執行,是併發進行的。

CMS垃圾回收器存在的問題

通過上文的介紹,相信小夥伴們對於CMS的基本工作原理有了一個認識,大家會發現CMS本身已經對垃圾回收機制進行了效能的優化,那麼為什麼我們在jvm調優時要減少Full GC的頻率呢?

其實CMS還是存在效能問題呢,比如上文我們說過的記憶體碎片問題。

cpu資源消耗問題

另外我們來思考一下,在併發標記階段和併發清理階段是最耗時的,與工作執行緒同時執行,是不是會導致CPU資源的佔用?

所以這兩個階段是比較耗費CPU資源的。

CMS預設啟動的垃圾回收執行緒數是(CPU核心數+3)/4。

那麼假如我們使用的是一個2核的處理器,那麼CMS就會佔用(2+3)/4=1個垃圾回收執行緒。

所以CMS這個併發機制的第一個問題就是消耗CPU的資源

Concurrent Mode Failure問題

第二個問題是比較嚴重的問題,就是在併發清理階段,CMS清理的其實是之前標記好的物件。

但是由於系統併發的執行著,所以可能會有新的物件進入老年代,同時變成垃圾物件,這種物件就是“浮動垃圾”。

因為他們雖然是垃圾物件,但沒有被標記,所以不會被清理掉。

所以為了保證CMS垃圾回收期間,還有一定的記憶體空間讓新物件進入老年代,一般會預留空間。

當老年代的記憶體佔用達到一定的比例值了,就會觸發Full GC。

“-XX:CMSInitiatingOccupancyFaction”引數可以設定這個比例值,jdk1.6裡面預設的是92%。

也就是說老年代佔用了92%的空間後,就會執行Full GC,預留8%空間給併發回收期間新進入老年代的物件。

那麼如果說這個預留的空間不夠了,會發生什麼呢?

這個時候就會發生Concurrent Mode Failure,然後會自動使用“Serial Old”垃圾回收器替代CMS,強行執行“Stop the World”,重新進行GC Roots追蹤,然後一次性回收掉垃圾物件後,再恢復系統工作程序。

這樣一來系統卡死的時間可能就很長了。

所以實際生產環境中,這個自動觸發GC的比例是可以合理優化一下的。但一般情況下都不需要優化。

記憶體碎片問題

記憶體碎片問題上文已經介紹過了,就是可能會頻繁引發Full GC。

CMS有個引數“-XX:+UseCMSCompactAtFullCollection”,預設是開啟的。

它的意思是在Full GC後要再次進行“Stop the World”,然後進行碎片整理工作。

還有一個引數“-XX:CMSFullGCsBeforeCompaction”,這個意思是執行多少次Full GC後再執行碎片整理,預設是0,意思是每次Full GC後進行碎片整理。

這兩個引數一般情況下都不需要修改,因為本來我們就要減少Full GC的頻率,在低頻率下,每次進行碎片整理是沒有問題的。

總結

今天我們對ParNew做了一個簡單的介紹,其實就是併發機制。同時比較詳細的介紹了CMS垃圾回收器的執行過程。

相信小夥伴們能夠對它們有一個深刻的印象,那麼新一代的G1垃圾回收器又是什麼機制呢?

下篇文章我們就一起對G1垃圾回收器進行探索,不見不散。

往期文章推薦:

大白話談JVM的類載入機制

JVM記憶體模型不再是祕密

輕鬆理解JVM的分代模型

秒懂JVM的垃圾回收機制