图解CMS垃圾回收机制,你值得拥有
簡書 占小狼,轉載請注明原創出處,謝謝!
最近在整理JVM相關的PPT,把CMS算法又過了一遍,每次閱讀源碼都能多了解一點,繼續堅持。
什么是CMS
CMS全稱 Concurrent Mark Sweep,是一款并發的、使用標記-清除算法的垃圾回收器,
如果老年代使用CMS垃圾回收器,需要添加虛擬機參數-"XX:+UseConcMarkSweepGC"。
使用場景:
GC過程短暫停,適合對時延要求較高的服務,用戶線程不允許長時間的停頓。
缺點:
服務長時間運行,造成嚴重的內存碎片化。
另外,算法實現比較復雜(如果也算缺點的話)
實現機制
根據GC的觸發機制分為:周期性Old GC(被動)和主動Old GC
個人理解,實在不知道怎么分才好。
周期性Old GC
周期性Old GC,執行的邏輯也叫Background Collect,對老年代進行回收,在GC日志中比較常見,由后臺線程ConcurrentMarkSweepThread循環判斷(默認2s)是否需要觸發。
觸發條件
1、如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,虛擬機會根據收集的數據決定是否觸發(建議線上環境帶上這個參數,不然會加大問題排查的難度)。
2、老年代使用率達到閾值 CMSInitiatingOccupancyFraction,默認92%。
3、永久代的使用率達到閾值 CMSInitiatingPermOccupancyFraction,默認92%,前提是開啟 CMSClassUnloadingEnabled。
4、新生代的晉升擔保失敗。
晉升擔保失敗
老年代是否有足夠的空間來容納全部的新生代對象或歷史平均晉升到老年代的對象,如果不夠的話,就提早進行一次老年代的回收,防止下次進行YGC的時候發生晉升失敗。
周期性Old GC過程
當條件滿足時,采用“標記-清理”算法對老年代進行回收,過程可以說很簡單,標記出存活對象,清理掉垃圾對象,但是為了實現整個過程的低延遲,實際算法遠遠沒這么簡單,整個過程分為如下幾個部分:
對象在標記過程中,根據標記情況,分成三類:
假設發生Background Collect時,Java堆的對象分布如下:
1、InitialMarking(初始化標記,整個過程STW)
該階段單線程執行,主要分分為兩步:
該過程結束后,對象分布如下:
2、Marking(并發標記)
該階段GC線程和應用線程并發執行,遍歷InitialMarking階段標記出來的存活對象,然后繼續遞歸標記這些對象可達的對象。
因為該階段并發執行的,在運行期間可能發生新生代的對象晉升到老年代、或者是直接在老年代分配對象、或者更新老年代對象的引用關系等等,對于這些對象,都是需要進行重新標記的,否則有些對象就會被遺漏,發生漏標的情況。
為了提高重新標記的效率,該階段會把上述對象所在的Card標識為Dirty,后續只需掃描這些Dirty Card的對象,避免掃描整個老年代。
3、Precleaning(預清理)
通過參數CMSPrecleaningEnabled選擇關閉該階段,默認啟用,主要做兩件事情:
4、AbortablePreclean(可中斷的預清理)
該階段發生的前提是,新生代Eden區的內存使用量大于參數CMSScheduleRemarkEdenSizeThreshold 默認是2M,如果新生代的對象太少,就沒有必要執行該階段,直接執行重新標記階段。
為什么需要這個階段,存在的價值是什么?
因為CMS GC的終極目標是降低垃圾回收時的暫停時間,所以在該階段要盡最大的努力去處理那些在并發階段被應用線程更新的老年代對象,這樣在暫停的重新標記階段就可以少處理一些,暫停時間也會相應的降低。
在該階段,主要循環的做兩件事:
當然了,這個邏輯不會一直循環下去,打斷這個循環的條件有三個:
如果在循環退出之前,發生了一次YGC,對于后面的Remark階段來說,大大減輕了掃描年輕代的負擔,但是發生YGC并非人為控制,所以只能祈禱這5s內可以來一次YGC。
... 1678.150: [CMS-concurrent-preclean-start] 1678.186: [CMS-concurrent-preclean: 0.044/0.055 secs] 1678.186: [CMS-concurrent-abortable-preclean-start] 1678.365: [GC 1678.465: [ParNew: 2080530K->1464K(2044544K), 0.0127340 secs] 1389293K->306572K(2093120K), 0.0167509 secs] 1680.093: [CMS-concurrent-abortable-preclean: 1.052/1.907 secs] ....在上面GC日志中,1678.186啟動了AbortablePreclean階段,在隨后不到2s就發生了一次YGC。
5、FinalMarking(并發重新標記,STW過程)
該階段并發執行,在之前的并行階段(GC線程和應用線程同時執行,好比你媽在打掃房間,你還在扔紙屑),可能產生新的引用關系如下:
上述對象中可能有一些已經在Precleaning階段和AbortablePreclean階段被處理過,但總存在沒來得及處理的,所以還有進行如下的處理:
在第一步驟中,需要遍歷新生代的全部對象,如果新生代的使用率很高,需要遍歷處理的對象也很多,這對于這個階段的總耗時來說,是個災難(因為可能大量的對象是暫時存活的,而且這些對象也可能引用大量的老年代對象,造成很多應該回收的老年代對象而沒有被回收,遍歷遞歸的次數也增加不少),如果在AbortablePreclean階段中能夠恰好的發生一次YGC,這樣就可以避免掃描無效的對象。
如果在AbortablePreclean階段沒來得及執行一次YGC,怎么辦?
CMS算法中提供了一個參數:CMSScavengeBeforeRemark,默認并沒有開啟,如果開啟該參數,在執行該階段之前,會強制觸發一次YGC,可以減少新生代對象的遍歷時間,回收的也更徹底一點。
不過,這種參數有利有弊,利是降低了Remark階段的停頓時間,弊的是在新生代對象很少的情況下也多了一次YGC,最可憐的是在AbortablePreclean階段已經發生了一次YGC,然后在該階段又傻傻的觸發一次。
所以利弊需要把握。
主動Old GC
這個主動Old GC的過程,觸發條件比較苛刻:
如果觸發了主動Old GC,這時周期性Old GC正在執行,那么會奪過周期性Old GC的執行權(同一個時刻只能有一種在Old GC在運行),并記錄 concurrent mode failure 或者 concurrent mode interrupted。
主動GC開始時,需要判斷本次GC是否要對老年代的空間進行Compact(因為長時間的周期性GC會造成大量的碎片空間),判斷邏輯實現如下:
*should_compact =UseCMSCompactAtFullCollection &&((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||GCCause::is_user_requested_gc(gch->gc_cause()) ||gch->incremental_collection_will_fail(true /* consult_young */));在三種情況下會進行壓縮:
帶壓縮動作的算法,稱為MSC,標記-清理-壓縮,采用單線程,全暫停的方式進行垃圾收集,暫停時間很長很長...
那不帶壓縮動作的算法是什么樣的呢?
不帶壓縮動作的執行邏輯叫Foreground Collect,整個過程相對周期性Old GC來說,少了Precleaning和AbortablePreclean兩個階段,其它過程都差不多。
如果執行System.gc(),而且添加了參數ExplicitGCInvokesConcurrent,這時并不屬于主動GC,它會推進周期性Old GC的進行,比如剛剛執行過一次,并不會等2s后檢查條件,而是立馬啟動周期性Old GC。
作者:占小狼
鏈接:https://www.jianshu.com/p/2a1b2f17d3e4
來源:簡書
總結
以上是生活随笔為你收集整理的图解CMS垃圾回收机制,你值得拥有的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java.lang.Instrument
- 下一篇: git add -u与-A .三者的区别