探索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的垃圾回收機制

 

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