詳解CMS垃圾回收機制

原創不易,未經容許,不得轉載~~~html

 

  1. 什麼是CMS?java

    Concurrent Mark Sweep。算法

    看名字就知道,CMS是一款併發、使用標記-清除算法的gc。數組

    CMS是針對老年代進行回收的GC。 服務器

  2. CMS有什麼用?微信

    CMS以獲取最小停頓時間爲目的。多線程

    在一些對響應時間有很高要求的應用或網站中,用戶程序不能有長時間的停頓,CMS 能夠用於此場景。 併發

  3. CMS如何執行?

     整體來講CMS的執行過程能夠分爲如下幾個階段:  oracle

          3.1 初始標記(STW)ide

          3.2 併發標記

          3.3 併發預清理

          3.4 重標記(STW)

          3.5 併發清理

          3.6 重置

 3.1 初始標記階段須要STW。

該階段進行可達性分析,標記GC ROOT能直接關聯到的對象。

注意是直接關聯間接關聯的對象在下一階段標記。


 3.2 併發標記階段是和用戶線程併發執行的過程。

該階段進行GC ROOT TRACING,在第一個階段被暫停的線程從新開始運行。

由前階段標記過的對象出發,全部可到達的對象都在本階段中標記。


 3.3 併發預處理階段作的工做仍是標記,與3.4的重標記功能類似。

既然類似爲何要有這一步?

前面咱們講過,CMS是以獲取最短停頓時間爲目的的GC。

重標記須要STW(Stop The World),所以重標記的工做盡量多的在併發階段完成來減小STW的時間。

此階段標記從新生代晉升的對象新分配到老年代的對象以及在併發階段被修改了的對象


 此階段比較複雜,從初學者容易忽略或者說不理解的地方拋出一個問題你們思考下

  • 如何肯定老年代的對象是活着的?

答案很簡單,經過GC ROOT TRACING可到達的對象就是活着的。

 繼續延伸,若是存在如下場景怎麼辦:

   
老年代進行GC時如何確保上圖中Current Obj標記爲活着的?

確認新生代的對象是活着的也存在相同問題,你們能夠思考下,文章後面會給出答案

 答案是必須掃描新生代來確保。這也是爲何CMS雖然是老年代的gc,但仍要掃描新生代的緣由。(注意初始標記也會掃描新生代)

在CMS日誌中咱們能夠清楚地看到掃描日誌:

[GC[YG occupancy: 820 K (6528 K)]

[Rescan (parallel) , 0.0024157 secs]

[weak refs processing, 0.0000143 secs]

[scrub string table, 0.0000258 secs] 

[1 CMS-remark: 479379K(515960K)] 480200K(522488K), 0.0025249 secs] 

[Times: user=0.01 sys=0.00, real=0.00 secs]

Rescan階段(remark階段的一個子階段)會掃描新生代和老年代中的對象。在日誌中能夠看到此階段標識爲Rescan (parallel),說明此階段是並行進行的。

(看到這裏,若是你心中仍有疑問說明已經入門了)

重點來了:全量的掃描新生代和老年代會不會很慢?確定會。

CMS號稱是停頓時間最短的GC,如此長的停頓時間確定是不能接受的。

如何解決呢?

大家先思考着。

 

 

 

 

 

 

 

必需要有一個可以快速識別新生代和老年代活着的對象的機制。

先說新生代。

你應該已經知道,新生代垃圾回收完剩下的對象全是活着的,而且活着的對象不多。

若是在掃描新生代前進行一次Minor GC,狀況是否是就變得好不少?

CMS 有兩個參數:CMSScheduleRemarkEdenSizeThresholdCMSScheduleRemarkEdenPenetration,默認值分別是2M、50%。兩個參數組合起來的意思是預清理後,eden空間使用超過2M時啓動可中斷的併發預清理(CMS-concurrent-abortable-preclean),直到eden空間使用率達到50%時中斷,進入remark階段。

若是能在可停止的預清理階段發生一次Minor GC,那就萬事大吉、天下太平了。 

這裏有一個小問題,可終止的預清理要執行多長時間來保證發生一次Minor GC?

答案是無法保證。道理很簡單,由於垃圾回收是JVM自動調度的,何時進行GC咱們控制不了。

但此階段總有一個執行時間吧?是的。

CMS提供了一個參數CMSMaxAbortablePrecleanTime ,默認爲5S。

只要到了5S,無論發沒發生Minor GC,有沒有到CMSScheduleRemardEdenPenetration都會停止此階段,進入remark。

若是在5S內仍是沒有執行Minor GC怎麼辦?

CMS提供CMSScavengeBeforeRemark參數,使remark前強制進行一次Minor GC。

這樣作利弊都有。好的一面是減小了remark階段的停頓時間;壞的一面是Minor GC後緊跟着一個remark pause。如此一來,停頓時間也比較久。

CMS日誌以下:

7688.150: [CMS-concurrent-preclean-start]

7688.186: [CMS-concurrent-preclean: 0.034/0.035 secs]

7688.186: [CMS-concurrent-abortable-preclean-start]

7688.465: [GC 7688.465: [ParNew: 1040940K->1464K(1044544K), 0.0165840 secs] 1343593K->304365K(2093120K), 

0.0167509 secs]7690.093: [CMS-concurrent-abortable-preclean: 1.012/1.907 secs]  7690.095: [GC[YG occupancy: 522484 K (1044544 K)]

7690.095: [Rescan (parallel) , 0.3665541 secs]7690.462: [weak refs processing, 0.0003850 secs] [1 CMS-remark: 302901K(1048576K)] 825385K(2093120K), 0.3670690 secs]

7688.186啓動了可終止的預清理,在隨後的三秒內啓動了Minor GC,而後進入了Remark階段.

實際上爲了減小remark階段的STW時間,預清理階段會盡量多作一些事情來減小remark停頓時間。

remark的rescan階段是多線程的,爲了便於多線程掃描新生代,預清理階段會將新生代分塊

每一個塊中存放着多個對象,這樣remark階段就不須要從頭開始識別每一個對象的起始位置。

多個線程的職責就很明確了,把分塊分配給多個線程,很快就掃描完。

遺憾的是,這種辦法仍然是創建在發生了Minor GC的條件下。

若是沒有發生Minor GC,top(下一個能夠分配的地址空間)如下的全部空間被認爲是一個塊(這個塊包含了新生代大部份內容)。

這種塊對於remark階段並不會起到多少做用,所以並行效率也會下降。


 ok,新生代的機制講完了,下面講講老年代。

老年代的機制與一個叫CARD TABLE的東西(這個東西其實就是個數組,數組中每一個位置存的是一個byte)密不可分。

CMS將老年代的空間分紅大小爲512bytes的塊,card table中的每一個元素對應着一個塊。

併發標記時,若是某個對象的引用發生了變化,就標記該對象所在的塊爲  dirty card

併發預清理階段就會從新掃描該塊,將該對象引用的對象標識爲可達。

舉個例子:

併發標記時對象的狀態:


但隨後current obj的引用發生了變化:

 

 current obj所在的塊被標記爲了dirty card。

 隨後到了pre-cleaning階段,還記得該階段的任務之一就是標記這些在併發標記階段被修改了的對象麼?以後那些經過current obj變得可達的對象也被標記了,變成下面這樣:


同時dirty card標誌也被清除。

老年代的機制就是這樣。

不過card table還有其餘做用

還記得前面提到的那個問題麼?進行Minor GC時,若是有老年代引用新生代,怎麼識別?

(有研究代表,在全部的引用中,老年代引用新生代這種場景不足1%.緣由你們能夠本身分析下)

當有老年代引用新生代,對應的card table被標識爲相應的值(card table中是一個byte,有八位,約定好每一位的含義就可區分哪一個是引用新生代,哪一個是併發標記階段修改過的)。

因此,Minor GC經過掃描card table就能夠很快的識別老年代引用新生代。

這裏點一下,hotspot 虛擬機使用字節碼解釋器、JIT編譯器、 write barrier維護 card table。

當字節碼解釋器或者JIT編譯器更新了引用,就會觸發write barrier操做card table.

再點一下,因爲card table的存在,當老年代空間很大時會發生什麼?(這裏你們能夠自由發揮想象

至此,預清理階段的工做講完。


3.4 重標記(STW)  暫停全部用戶線程,從新掃描堆中的對象,進行可達性分析,標記活着的對象。

有了前面的基礎,這個階段的工做量被大大減輕,停頓時間所以也會減小。

注意這個階段是多線程的。


 3.5 併發清理。用戶線程被從新激活,同時清理那些無效的對象。


 3.6 重置。 CMS清除內部狀態,爲下次回收作準備。 


CMS執行過程講完了,重點講解了併發預清理時的操做及CMS幾個關鍵參數。大家能夠消化一下,消化完了能夠休息一下,由於事情還沒結束。

 4. CMS有什麼問題?

every coin has two sides ------高中英語做文我常常用的一句話。

在我看來,CMS這三個字母就隱含了問題所在併發+標記-清除算法 是問題的來源。 

先說併發

4.1併發意味着多線程搶佔CPU資源,即GC線程與用戶線程搶佔CPU。這可能會形成用戶線程執行效率降低。

CMS默認的回收線程數是(CPU個數+3)/4。這個公式的意思是當CPU大於4個時,保證回收線程佔用至少25%的CPU資源,這樣用戶線程佔用75%的CPU,這是能夠接受的。

可是,若是CPU資源不多,好比只有兩個的時候怎麼辦?按照上面的公式,CMS會啓動1個GC線程。至關於GC線程佔用了50%的CPU資源,這就可能致使用戶程序的執行速度突然下降了50%,50%已是很明顯的下降了。

這種場景怎麼處理呢?

我給的答案是能夠不用考慮這種場景。如今的PC機中都至少有雙核處理器,更別說大型的服務器了。

CMS的解決方案是提供了一個 incremental mode(增量模式)。

在這種模式下,進行併發標記、清理時讓GC線程、用戶線程交替運行,儘可能減小GC線程獨佔CPU資源的時間。

這會形成GC時間更長,但對用戶線程形成的影響就會少一些。

但實踐證實,這種模式下CMS的表現很通常,並無什麼大的優化。

i-CMS已經被聲明爲「deprecated」,再也不提倡使用。

(https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector)

 

4.2 併發清理階段用戶線程還在運行,這段時間就可能產生新的垃圾,新的垃圾在這次GC沒法清除,只能等到下次清理。這些垃圾有個專業名詞:浮動垃圾

因爲垃圾回收階段用戶線程仍在執行,必需預留出內存空間給用戶線程使用。所以不能像其餘回收器那樣,等到老年代滿了再進行GC。

CMS 提供了CMSInitiatingOccupancyFraction參數來設置老年代空間使用百分比,達到百分比就進行垃圾回收。

這個參數默認是92%,參數選擇須要看具體的應用場景。

設置的過小會致使頻繁的CMS GC,產生大量的停頓;反過來想,設置的過高會發生什麼?

假設如今設置爲99%,還剩1%的空間可使用。

在併發清理階段,若用戶線程須要使用的空間大於1%,就會產生Concurrent  Mode Failure錯誤,意思就是說併發模式失敗。

這時,虛擬機就會啓動備案:使用Serial Old收集器從新對老年代進行垃圾回收.如此一來,停頓時間變得更長。

因此CMSInitiatingOccupancyFraction的設置要具體問題具體分析。

網上有一些設置此參數的公式,我的認爲不是很嚴謹(緣由就是CMS另一個問題致使的),所以不寫出來以避免你們疑惑。

 

其實CMS有動態檢查機制

CMS會根據歷史記錄,預測老年代還須要多久填滿及進行一次回收所須要的時間。

在老年代空間用完以前,CMS能夠根據本身的預測自動執行垃圾回收。

這個特性可使用參數UseCMSInitiatingOccupancyOnly來關閉。

 

這裏提個問題給讀者思考,若是讓你設計,如何預測何時開始自動執行

 

4.3 前兩個問題是由併發引發的,接下來要說的問題就是由標記-清除算法引發的。

使用標記-清除算法可能形成大量的空間碎片。空間碎片過多,就會給大對象分配帶來麻煩。

每每老年代還有很大剩餘空間,但沒法找到足夠大的連續空間來分配當前對象,不得不觸發一次Full GC。

CMS的解決方案是使用UseCMSCompactAtFullCollection參數(默認開啓),在頂不住要進行Full GC時開啓內存碎片整理。

這個過程須要STW,碎片問題解決了,但停頓時間又變長了。

虛擬機還提供了另一個參數CMSFullGCsBeforeCompaction,用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認爲0,每次進入Full GC時都進行碎片整理)。

延伸一個「foreground collector」的東西給你們,這個玩意在Java8中也聲明爲deprecated。(https://bugs.openjdk.java.net/browse/JDK-8027132)

CMS存在的問題已經講清楚,你們消化下。


至此,CMS相關內容已經講完。

 

總結一下:

CMS採用了多種方式儘量下降GC的暫停時間,減小用戶程序停頓。

停頓時間下降的同時犧牲了CPU吞吐量 。

這是在停頓時間和性能間作出的取捨,能夠簡單理解爲"空間(性能)"換時間。

 

文中提到的幾個問題你們能夠把本身當成設計者來思考

 

再次聲明,未經容許,不得轉載!

歡迎關注微信訂閱號:

參考資料:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector

https://blogs.oracle.com/jonthecollector/entry/did_you_know

http://dept.cs.williams.edu/~freund/cs434/hotspot-gc.pdf

https://plumbr.eu/handbook/garbage-collection-algorithms-implementations

https://blogs.msdn.microsoft.com/abhinaba/2009/03/02/back-to-basics-generational-garbage-collection/

https://bugs.openjdk.java.net/browse/JDK-8027132

《深刻理解Java虛擬機 JVM高級特性與最佳實踐》

相關文章
相關標籤/搜索