JAVA虛擬機之三:CMS垃圾收集器

一、CMS垃圾收集器介紹

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於“標記-清除”算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分爲4個步驟,包括:
初始標記(CMS initial mark)
併發標記(CMS concurrent mark)
重新標記(CMS remark)
併發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
由於整個過程中耗時最長的併發標記和併發清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發地執行的。通過下圖可以比較清楚地看到CMS收集器的運作步驟中併發和需要停頓的時間。

 CMS是一款優秀的收集器,它的最主要優點在名字上已經體現出來了:併發收集、低停頓,Sun的一些官方文檔裏面也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)。但是CMS還遠達不到完美的程度,它有以下三個顯著的缺點:
  • CMS收集器對CPU資源非常敏感。其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啓動的回收線程數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,併發回收時垃圾收集線程最多佔用不超過25%的CPU資源。但是當CPU不足4個時(譬如2個),那麼CMS對用戶程序的影響就可能變得很大,如果CPU負載本來就比較大的時候,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了50%,這也很讓人受不了。爲了解決這種情況,虛擬機提供了一種稱爲“增量式併發收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器變種,所做的事情和單CPU年代PC機操作系統使用搶佔式來模擬多任務機制的思想一樣,就是在併發標記和併發清理的時候讓GC線程、用戶線程交替運行,儘量減少GC線程的獨佔資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得少一些,速度下降也就沒有那麼明顯,但是目前版本中,i-CMS已經被聲明爲“deprecated”,即不再提倡用戶使用。
  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段用戶線程還在運行着,伴隨程序的運行自然還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理掉它們,只好留待下一次GC時再將其清理掉。這一部分垃圾就稱爲“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,即還需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,如果在應用中老年代增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低內存回收次數以獲取更好的性能。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時候虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置得太高將會很容易導致大量“Concurrent Mode Failure”失敗,性能反而降低。
  • 還有最後一個缺點,在本節在開頭說過,CMS是一款基於“標記-清除”算法實現的收集器,如果讀者對前面這種算法介紹還有印象的話,就可能想到這意味着收集結束時會產生大量空間碎片。空間碎片過多時,將會給大對象分配帶來很大的麻煩,往往會出現老年代還有很大的空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數,用於在“享受”完Full GC服務之後額外免費附送一個碎片整理過程,內存整理的過程是無法併發的。空間碎片問題沒有了,但停頓時間不得不變長了。虛擬機設計者們還提供了另外一個參數-XX: CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。
 
二、CMS收集器測試
在eclipse中配置參數如下圖:

 完整配置參數:
-server -verbose:gc -Xms512m -Xmx512m -Xmn192m -XX:PermSize=32m -XX:MaxPermSize=32m -Xss256k -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5000 -XX:CMSFullGCsBeforeCompaction=5 -XX:CMSInitiatingOccupancyFraction=85 -XX:+UseParNewGC -Xloggc:D:/logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/logs/HeapDumpOnOutOfMemoryError.log -XX:+DisableExplicitGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
代碼Test1.java
package com.tools138.com;
/**
 *
 * @author alaric
 *
 */
public class Test2 {

 private static int n = 20;
 /**
  * @param args
  * @throws InterruptedException
  */
 public static void main(String[] args) throws InterruptedException {
  // TODO Auto-generated method stub
 
         byte[] b1 = getM(50);  
         byte[] b2 = getM(50);  
         byte[] b3 = getM(50);  
         byte[] b4 = getM(50);  
         byte[] b5 = getM(50);  
         byte[] b6 = getM(50);  
         byte[] b7 = getM(5);  
         byte[] b8 = getM(5);  
         byte[] b9 = getM(5);
         byte[] b10 = getM(5);
         byte[] b11 = getM(5);
         byte[] b12 = getM(5);
         byte[] b13 = getM(5);
         byte[] b14 = getM(5);
         byte[] b15 = getM(5);
         byte[] b16 = getM(5);
         byte[] b17 = getM(5);
         byte[] b18 = getM(5);
         byte[] b19 = getM(5);
         byte[] b20 = getM(100);
         byte[] b21 = getM(100);
         byte[] b22 = getM(100);
         byte[] b23 = getM(100);
         
   // Thread.sleep(2000);
 
     }  
   
     public static byte[] getM(int m) {  
         return new byte[1024 * 1024 * m];  
     }  

}
因爲用的是JDK1.8測試來測試的,所以PermSize,MaxPermSize已經在java8中移除,UseCMSCompactAtFullCollection,CMSFullGCsBeforeCompaction已經過時。 控制檯輸出如下:
上面的java測試代碼可以隨意進行添加修改,來測試CMS收集器的收集的各個階段。
gc.log輸出:
Java HotSpot(TM) 64-Bit Server VM (25.65-b01) for windows-amd64 JRE (1.8.0_65-b17), built on Oct  6 2015 16:39:20 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4094316k(1410832k free), swap 8186796k(3113544k free)
CommandLine flags: -XX:CMSFullGCsBeforeCompaction=5 -XX:CMSInitiatingOccupancyFraction=85 -XX:CMSMaxAbortablePrecleanTime=5000 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/logs/HeapDumpOnOutOfMemoryError.log -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=1048144896 -XX:MaxNewSize=201326592 -XX:MaxTenuringThreshold=6 -XX:NewSize=201326592 -XX:OldPLABSize=16 -XX:ParallelGCThreads=4 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ThreadStackSize=256 -XX:+UseCMSCompactAtFullCollection -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
0.279: [GC (Allocation Failure) 0.279: [ParNew: 114985K->553K(176960K), 0.0395416 secs] 114985K->102956K(504640K), 0.0398840 secs] [Times: user=0.17 sys=0.00, real=0.04 secs]
0.353: [GC (Allocation Failure) 0.353: [ParNew: 157128K->715K(176960K), 0.1367165 secs] 259530K->256719K(504640K), 0.1368421 secs] [Times: user=0.47 sys=0.01, real=0.14 secs]
0.498: [GC (CMS Initial Mark) [1 CMS-initial-mark: 256004K(327680K)] 307919K(504640K), 0.0002677 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.498: [CMS-concurrent-mark-start]
0.506: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.02 sys=0.02, real=0.01 secs]
0.506: [CMS-concurrent-preclean-start]
0.507: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.507: [CMS-concurrent-abortable-preclean-start]
0.507: [GC (Allocation Failure) 0.507: [ParNew: 121133K->16165K(176960K), 0.0562811 secs] 377137K->374569K(540508K), 0.0563903 secs] [Times: user=0.19 sys=0.01, real=0.06 secs]
0.577: [GC (Allocation Failure) 0.577: [ParNew: 118565K->0K(176960K), 0.0739984 secs] 476969K->476703K(658284K), 0.0741047 secs] [Times: user=0.22 sys=0.02, real=0.07 secs]
0.652: [CMS-concurrent-abortable-preclean: 0.001/0.145 secs] [Times: user=0.41 sys=0.03, real=0.14 secs]
0.667: [GC (CMS Final Remark) [YG occupancy: 102400 K (176960 K)]0.667: [Rescan (parallel) , 0.0247611 secs]0.692: [weak refs processing, 0.0000398 secs]0.692: [class unloading, 0.0002673 secs]0.692: [scrub symbol table, 0.0004401 secs]0.693: [scrub string table, 0.0001223 secs][1 CMS-remark: 476703K(481324K)] 579103K(658284K), 0.0258248 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
0.693: [CMS-concurrent-sweep-start]
0.693: [GC (Allocation Failure) 0.693: [ParNew: 102400K->0K(176960K), 0.1390355 secs] 579103K->579103K(760688K), 0.1391037 secs] [Times: user=0.31 sys=0.03, real=0.14 secs]
0.832: [CMS-concurrent-sweep: 0.139/0.139 secs] [Times: user=0.31 sys=0.03, real=0.14 secs]
0.833: [CMS-concurrent-reset-start]
0.836: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.851: [GC (Allocation Failure) 0.851: [ParNew: 102400K->0K(176960K), 0.1025667 secs] 681504K->681504K(1004352K), 0.1026574 secs] [Times: user=0.14 sys=0.08, real=0.10 secs]
0.968: [GC (CMS Initial Mark) [1 CMS-initial-mark: 681504K(827392K)] 783904K(1004352K), 0.0001363 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.968: [CMS-concurrent-mark-start]
Heap
 par new generation   total 176960K, used 103973K [0x00000000c1800000, 0x00000000cd800000, 0x00000000cd800000)
  eden space 157312K,  66% used [0x00000000c1800000, 0x00000000c7d89600, 0x00000000cb1a0000)
  from space 19648K,   0% used [0x00000000cb1a0000, 0x00000000cb1a0000, 0x00000000cc4d0000)
  to   space 19648K,   0% used [0x00000000cc4d0000, 0x00000000cc4d0000, 0x00000000cd800000)
 concurrent mark-sweep generation total 827392K, used 681504K [0x00000000cd800000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 2612K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 287K, capacity 386K, committed 512K, reserved 1048576K
主要關注一下紅色框起來的部分。
對上圖中一條完整收集記錄進行解釋:
[ParNew:  表示年輕代GC,用ParNew 收集器收集;
157128K->715K(176960K) :157128K 表示年輕代收集前的大小,715K表示收集後的大小,(176960K)表示當前總容量大小;
, 0.1367165 secs] :表示收集年輕代所花費的時間,單位秒;
 259530K->256719K(504640K), 0.1368421 secs]:表示當前對的收集前大小,收集後大小和容量總大小及收集花費的時間。
[Times: user=0.47 sys=0.01, real=0.14 secs]: 表示用戶耗時,系統耗時,真實耗時
 
再回到上面大圖,看紅色框起來的數據:
0.279秒和0.353秒時候兩次ParNew在新生代分配失敗(Allocation Failure),對象直接進入老年代;
0.498秒時老年代CMS初始標記(Initial-mark);
0.498秒進行併發標記(concurrent-mark-start);
0.506秒開始進行預清理(concurrent-preclean-start);
0.667秒(CMS Final Remark);
0.693秒開始併發清理開始(concurrent-sweep);
0.833秒線程參數重置(reset);
 
三、CMS垃圾收集器總結
當使用CMS收集器時,當開始進行收集時,old代的收集過程如下所示:
1、首先jvm根據-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什麼時間開始垃圾收集;
2、如果設置了-XX:+UseCMSInitiatingOccupancyOnly,那麼只有當old代佔用確實達到了-XX:CMSInitiatingOccupancyFraction參數所設定的比例時纔會觸發cms gc;
3、如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,那麼系統會根據統計數據自行決定什麼時候觸發cms gc;因此有時會遇到設置了80%比例才cms gc,但是50%時就已經觸發了,就是因爲這個參數沒有設置的原因;
4、當cms gc開始時,首先的階段是CMS-initial-mark,此階段是初始標記階段,是stop the world階段,因此此階段標記的對象只是從root集最直接可達的對象;
     [1 CMS-initial-mark: 256004K(327680K)] 307919K(504640K),,指標記時,old代的已用空間和總空間
5、下一個階段是CMS-concurrent-mark,此階段是和應用線程併發執行的,所謂併發收集器指的就是這個,主要作用是標記可達的對象
       此階段會打印2條日誌:CMS-concurrent-mark-start,CMS-concurrent-mark
6、下一個階段是CMS-concurrent-preclean,此階段主要是進行一些預清理,因爲標記和應用線程是併發執行的,因此會有些對象的狀態在標記後會改變,此階段正是解決這個問題因爲之後的Rescan階段也會stop the world,爲了使暫停的時間儘可能的小,也需要preclean階段先做一部分工作以節省時間
     此階段會打印2條日誌:CMS-concurrent-preclean-start,CMS-concurrent-preclean
7、下一階段是CMS-concurrent-abortable-preclean階段,加入此階段的目的是使cms gc更加可控一些,作用也是執行一些預清理,以減少Rescan階段造成應用暫停的時間
     此階段涉及幾個參數:
     -XX:CMSMaxAbortablePrecleanTime:當abortable-preclean階段執行達到這個時間時纔會結束
     -XX:CMSScheduleRemarkEdenSizeThreshold(默認2m):控制abortable-preclean階段什麼時候開始執行,
      即當eden使用達到此值時,纔會開始abortable-preclean階段
     -XX:CMSScheduleRemarkEdenPenetratio(默認50%):控制abortable-preclean階段什麼時候結束執行
      此階段會打印一些日誌如下:
     CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
      CMS:abort preclean due to time XXX
8、再下一個階段是第二個stop the world階段了,即Rescan階段,此階段暫停應用線程,對對象進行重新掃描並標記;
      [YG occupancy: 102400 K (176960 K)],指執行時young代的情況
      [1 CMS-remark: 476703K(481324K)] ,指執行時old代的情況
      此外,還打印出了弱引用處理、類卸載等過程的耗時
9、再下一個階段是CMS-concurrent-sweep,進行併發的垃圾清理
10、最後是CMS-concurrent-reset,爲下一次cms gc重置相關數據結構
11、full gc:
有2種情況會觸發full gc,在full gc時,整個應用會暫停
       A,concurrent-mode-failure:當cms gc正進行時,此時有新的對象要進行old代,但是old代空間不足造成的
       B,promotion-failed:當進行young gc時,有部分young代對象仍然可用,但是S1或S2放不下,因此需要放到old代,但此時old代空間無法容納此。
影響cms gc時長及觸發的參數是以下2個: 
        -XX:CMSMaxAbortablePrecleanTime=5000
        -XX:CMSInitiatingOccupancyFraction=80
解決也是針對這兩個參數來的,根本的原因是每次請求消耗的內存量過大
解決方式:
      A,針對cms gc的觸發階段,調整-XX:CMSInitiatingOccupancyFraction=50,提早觸發cms gc,就可以緩解當old代達到80%,cms gc處理不完,從而造成concurrent mode failure引發full gc
     B,修改-XX:CMSMaxAbortablePrecleanTime=500,縮小CMS-concurrent-abortable-preclean階段的時間
     C,考慮到cms gc時不會進行compact,因此加入-XX:+UseCMSCompactAtFullCollection
       (cms gc後會進行內存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次後會進行compact)參數
 
 
參考資料:
1、深入理解java虛擬機-周志民
2、http://www.importnew.com/13954.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章