GC 知識點補充——CMS

  • 2019 年 11 月 1 日
  • 筆記

之前已經講過了不少有關 GC 的內容,今天準備將之前沒有細講的部分進行補充,首先要提到的就是垃圾收集器。

基礎的回收方式有三種:清除、壓縮、複製,衍生出來的垃圾收集器有:

Serial 收集器

新生代收集器,使用停止複製演算法,使用一個執行緒進行 GC ,串列,其它工作執行緒暫停。

使用-XX:+UseSerialGC開關來控制使用Serial + Serial Old模式運行進行記憶體回收(這也是虛擬機在 Client 模式下運行的默認值)。

ParNew 收集器

新生代收集器,使用停止複製演算法,Serial 收集器的多執行緒版,用多個執行緒進行 GC ,並行,其它工作執行緒暫停,關注縮短垃圾收集時間。

使用-XX:+UseParNewGC開關來控制使用ParNew + Serial Old收集器組合收集記憶體;使用-XX:ParallelGCThreads來設置執行記憶體回收的執行緒數。

Parallel Scavenge 收集器

新生代收集器,使用停止複製演算法,關注 CPU 吞吐量,即運行用戶程式碼的時間/總時間,比如:JVM 運行 100 分鐘,其中運行用戶程式碼 99 分鐘,垃 圾收集 1 分鐘,則吞吐量是 99% ,這種收集器能最高效率的利用 CPU ,適合運行後台運算(其他關注縮短垃圾收集時間的收集器,如 CMS ,等待時間很少,所以適 合用戶交互,提高用戶體驗)。

使用-XX:+UseParallelGC開關控制使用Parallel Scavenge + Serial Old收集器組合回收垃圾(這也是在 Server 模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間佔總時間的比例,默認 99 ,即 1% 的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置 GC 的最大停頓時間(這個參數只對 Parallel Scavenge 有效),用開關參數-XX:+UseAdaptiveSizePolicy可以進行動態控制,如自動調整 Eden / Survivor 比例,老年代對象年齡,新生代大小等,這個參數在 ParNew 下沒有。

Serial Old 收集器

老年代收集器,單執行緒收集器,串列,使用標記-整理演算法,使用單執行緒進行GC,其它工作執行緒暫停(注意:在老年代中進行標記-整理演算法清理,也需要暫停其它執行緒),在JDK1.5之前,Serial Old 收集器與 ParallelScavenge 搭配使用。

整理的方法是 Sweep (清除)和 Compact (壓縮),清除是將廢棄的對象幹掉,只留倖存的對象,壓縮是移動對象,將空間填滿保證記憶體分為2塊,一塊全是對象,一塊空閑),

Parallel Old 收集器

老年代收集器,多執行緒,並行,多執行緒機制與 Parallel Scavenge 差不錯,使用標記-整理演算法,在 Parallel Old 執行時,仍然需要暫停其它工作執行緒。

Parallel Old 收集器的整理,與 Serial Old 不同,這裡的整理是Copy(複製)和Compact(壓縮),複製的意思就是將倖存的對象複製到預先準備好的區域,而不是像Sweep(清除)那樣清除廢棄的對象。

Parallel Old 在多核計算中很有用。 Parallel Old 出現後(JDK 1.6),與 Parallel Scavenge 配合有很好的效果,充分體現 Parallel Scavenge 收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge + Parallel Old組合收集器進行收集。

CMS

全稱 Concurrent Mark Sweep,老年代收集器,致力於獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標記-清除演算法,多執行緒,優點是並發收集(用戶執行緒可以和 GC 執行緒同時工作),停頓小。

使用-XX:+UseConcMarkSweepGC進行ParNew + CMS + Serial Old進行記憶體回收,優先使用ParNew + CMS(原因見後面),當用戶執行緒記憶體不足時,採用備用方案Serial Old收集。

如何開始

首先來看一下 CMS 是在什麼情況下進行 GC:

  1. 首先 JVM 根據-XX:CMSInitiatingOccupancyFraction、-XX:+UseCMSInitiatingOccupancyOnly來決定什麼時間開始垃圾收集。
  2. 如果設置了-XX:+UseCMSInitiatingOccupancyOnly,那麼只有當老年代佔用確實達到了-XX:CMSInitiatingOccupancyFraction參數所設定的比例時才會觸發 CMS GC。
  3. 如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,那麼系統會根據統計數據自行決定什麼時候觸發 CMS GC。因此有時會遇到設置了 80% 比例才 CMS GC,但是 50% 時就已經觸發了,就是因為這個參數沒有設置的原因。

具體執行

CMS GC 的執行過程,具體來說就是:

初始標記(CMS-initial-mark)

該階段是 stop the world 階段,因此此階段標記的對象只是從 root 集最直接可達的對象。

此階段會列印 1 條日誌:CMS-initial-mark:961330K(1572864K),指標記時,老年代的已用空間和總空間

並發標記(CMS-concurrent-mark)

此階段是和應用執行緒並發執行的,所謂並發收集器指的就是這個,主要作用是標記可達的對象,此階段不需要用戶執行緒停頓。

此階段會列印 2 條日誌:CMS-concurrent-mark-start,CMS-concurrent-mark

預清理(CMS-concurrent-preclean)

此階段主要是進行一些預清理,因為標記和應用執行緒是並發執行的,因此會有些對象的狀態在標記後會改變,此階段正是解決這個問題。因為之後的 CMS-remark 階段也會 stop the world,為了使暫停的時間儘可能的小,也需要 preclean 階段先做一部分工作以節省時間。

此階段會列印 2 條日誌:CMS-concurrent-preclean-start,CMS-concurrent-preclean

可控預清理(CMS-concurrent-abortable-preclean)

此階段的目的是使 CMS GC 更加可控一些,作用也是執行一些預清理,以減少 CMS-remark 階段造成應用暫停的時間。

此階段涉及幾個參數:

    -XX:CMSMaxAbortablePrecleanTime:當 abortable-preclean 階段執行達到這個時間時才會結束。      -XX:CMSScheduleRemarkEdenSizeThreshold(默認2m):控制 abortable-preclean 階段什麼時候開始執行,即當年輕代使用達到此值時,才會開始 abortable-preclean 階段。      -XX:CMSScheduleRemarkEdenPenetratio(默認50%):控制 abortable-preclean 階段什麼時候結束執行。

此階段會列印 3 條日誌:CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,CMS:abort preclean due to time XXX

重新標記(CMS-remark)

此階段暫停應用執行緒,停頓時間比並發標記小得多,但比初始標記稍長,因為會對所有對象進行重新掃描並標記。

此階段會列印以下日誌:

  1. YG occupancy:964861K(2403008K),指執行時年輕代的情況。
  2. CMS remark:961330K(1572864K),指執行時老年代的情況。
  3. 此外,還列印出了弱引用處理、類卸載等過程的耗時。

並發清除(CMS-concurrent-sweep)

此階段進行並發的垃圾清理。

並發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)

此階段是為下一次 CMS GC 重置相關數據結構。

總結

CMS 的收集過程,概括一下就是:2 次標記,2 次預清除,1 次重新標記,1 次清除。

在CMS清理過程中,只有初始標記和重新標記需要短暫停頓用戶執行緒,並發標記和並發清除都不需要暫停用戶執行緒,因此效率很高,很適合高交互的場合。

CMS也有缺點,它需要消耗額外的 CPU 和記憶體資源。在 CPU 和記憶體資源緊張,會加重系統負擔(CMS 默認啟動執行緒數為( CPU數量 + 3 ) / 4 )。

另外,在並發收集過程中,用戶執行緒仍然在運行,仍然產生記憶體垃圾,所以可能產生「浮動垃圾」(本次無法清理,只能下一次Full GC才清理)。因此在 GC 期間,需要預留足夠的記憶體給用戶執行緒使用。

所以使用 CMS 的收集器並不是老年代滿了才觸發 Full GC ,而是在使用了一大半(默認 68% ,即 2/3 ,使用-XX:CMSInitiatingOccupancyFraction來設置)的時候就要進行 Full GC。如果用戶執行緒消耗記憶體不是特別大,可以適當調高-XX:CMSInitiatingOccupancyFraction以降低 GC 次數,提高性能。如果預留的用戶執行緒記憶體不夠,則會觸發 Concurrent Mode Failure,此時,將觸發備用方案:使用 Serial Old 收集器進行收集,但這樣停頓時間就長了,因此-XX:CMSInitiatingOccupancyFraction不宜設的過大。

還有,CMS 採用的是標記-清除演算法,會導致記憶體碎片的產生,可以使用-XX:+UseCMSCompactAtFullCollection來設置是否在 Full GC 之後進行碎片整理,用-XX:CMSFullGCsBeforeCompaction來設置在執行多少次不壓縮的 Full GC 之後,來一次帶壓縮的 Full GC。

並發和並行

並發收集:

指用戶執行緒與GC執行緒同時執行(不一定是並行,可能交替,但總體上是在同時執行的),不需要停頓用戶執行緒(其實在 CMS 中用戶執行緒還是需要停頓的,只是非常短,GC 執行緒在另一個 CPU 上執行);

並行收集:

指多個 GC 執行緒並行工作,但此時用戶執行緒是暫停的;

所以,Serial 是串列的,Parallel 收集器是並行的,而 CMS 收集器是並發的。

總結

今天了解了一下普通的垃圾收集器,並且詳細介紹了 CMS,其特性其實是基於普通的垃圾演算法,增加了預處理、預清除的過程,因此效率更加優越。當然它也有自己的缺點,更加消耗資源,因此在選用的時候需要結合實際場景。

有興趣的話可以訪問我的部落格或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/

Tags: