CMS垃圾收集器

1、CMS垃圾收集器介紹
衆所周知,在oracle公司的Hotspot的架構中,大致上採用分代回收的機制。其中出生代又採用了拷貝複製的方法。若是對象在初生代內存活超過必定次數以後,就能夠晉升到老生代中,而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須要較大的內存空間去運行垃圾收集,此時用戶程序也在運行,要是CMS運行期 間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failure」失敗,這時候虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。因此說參數-XX:CMSInitiatingOccupancyFraction設置 得過高將會很容易致使大量「Concurrent Mode Failure」失敗,性能反而下降。
  • 還有最後一個缺點,在本節在開頭說過,CMS是一款基於「標記-清除」算法實現的收集器,若是讀者對前面這種算法介紹還有印象的話,就可能想到這 意味着收集結束時會產生大量空間碎片。空間碎片過多時,將會給大對象分配帶來很大的麻煩,每每會出現老年代還有很大的空間剩餘,可是沒法找到足夠大的連續 空間來分配當前對象,不得不提早觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數,用於在「享受」完 Full GC服務以後額外免費附送一個碎片整理過程,內存整理的過程是沒法併發的。空間碎片問題沒有了,但停頓時間不得不變長了。虛擬機設計者們還提供了另一個 參數-XX: CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。
2、CMS收集器測試
虛擬機參數:
-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

測試代碼java

public class Test2 { public static void main(String[] args) { 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); } public static byte[] getM(int m) { return new byte[1024 * 1024 * m]; } }

由於用的是JDK1.8測試來測試的,因此PermSize,MaxPermSize已經在java8中移除,UseCMSCompactAtFullCollection,CMSFullGCsBeforeCompaction已通過時。 控制檯輸出以下:算法

上面的java測試代碼能夠隨意進行添加修改,來測試CMS收集器的收集的各個階段。
gc.log輸出:
"C:\Program Files\Java\jdk1.8.0_111\bin\java" -Dvisualvm.id=271965950114811 -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 -XX:+HeapDumpOnOutOfMemoryError -XX:+DisableExplicitGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 0.268: [GC (Allocation Failure) 0.268: [ParNew: 114985K->1537K(176960K), 0.0314708 secs] 114985K->103939K(504640K), 0.0315495 secs] [Times: user=0.11 sys=0.02, real=0.03 secs] 0.322: [GC (Allocation Failure) 0.323: [ParNew: 158154K->1842K(176960K), 0.0473010 secs] 260556K->257847K(504640K), 0.0473510 secs] [Times: user=0.14 sys=0.03, real=0.05 secs] 0.377: [GC (CMS Initial Mark) [1 CMS-initial-mark: 256004K(327680K)] 309047K(504640K), 0.0010189 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 0.378: [CMS-concurrent-mark-start] 0.382: [CMS-concurrent-mark: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 0.382: [CMS-concurrent-preclean-start] 0.382: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 0.383: [CMS-concurrent-abortable-preclean-start] 0.383: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 0.383: [GC (Allocation Failure) 0.383: [ParNew: 125436K->125436K(176960K), 0.0000202 secs]0.383: [CMS (concurrent mode failure): 256004K->322560K(327680K), 0.0757518 secs] 381440K->374947K(504640K), [Metaspace: 3502K->3502K(1056768K)], 0.0758187 secs] [Times: user=0.05 sys=0.02, real=0.08 secs] 0.466: [Full GC (Allocation Failure) 0.466: [CMS: 322560K->322560K(327680K), 0.0028732 secs] 477347K->477347K(504640K), [Metaspace: 3502K->3502K(1056768K)], 0.0029323 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 0.469: [Full GC (Allocation Failure) 0.469: [CMS: 322560K->322560K(327680K), 0.0203221 secs] 477347K->477315K(504640K), [Metaspace: 3502K->3502K(1056768K)], 0.0203582 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid41232.hprof ... 0.490: [GC (CMS Initial Mark) [1 CMS-initial-mark: 322560K(327680K)] 477315K(504640K), 0.0007328 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 0.490: [CMS-concurrent-mark-start] Heap dump file created [489779136 bytes in 9.580 secs] 10.070: [CMS-concurrent-mark: 0.002/9.579 secs] [Times: user=0.02 sys=0.42, real=9.58 secs] 10.070: [CMS-concurrent-preclean-start] 10.071: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 10.071: [CMS-concurrent-abortable-preclean-start] 10.071: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 10.071: [GC (CMS Final Remark) [YG occupancy: 157312 K (176960 K)]10.071: [Rescan (parallel) Exception in thread "main" , 0.0016064 secs]10.073: [weak refs processing, 0.0000323 secs]10.073: [class unloading, 0.0009380 secs]10.074: [scrub symbol table, 0.0009629 secs]10.075: [scrub string table, 0.0003188 secs][1 CMS-remark: 322560K(327680K)] 479872K(504640K), 0.0040970 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 10.076: [CMS-concurrent-sweep-start] 10.076: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 10.076: [CMS-concurrent-reset-start] 10.077: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap par new generation   total 176960K, used 157312K [0x00000000e0000000, 0x00000000ec000000, 0x00000000ec000000) eden space 157312K, 100% used [0x00000000e0000000, 0x00000000e99a0000, 0x00000000e99a0000) from space 19648K, 0% used [0x00000000e99a0000, 0x00000000e99a0388, 0x00000000eacd0000) to space 19648K, 0% used [0x00000000eacd0000, 0x00000000eacd0000, 0x00000000ec000000) concurrent mark-sweep generation total 327680K, used 322560K [0x00000000ec000000, 0x0000000100000000, 0x0000000100000000) Metaspace used 3534K, capacity 4494K, committed 4864K, reserved 1056768K class space used 384K, capacity 386K, committed 512K, reserved 1048576K java.lang.OutOfMemoryError: Java heap space at Test2.getM(Test2.java:44) at Test2.main(Test2.java:35) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=32m; support was removed in 8.0 Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=32m; support was removed in 8.0 Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release. Java HotSpot(TM) 64-Bit Server VM warning: CMSFullGCsBeforeCompaction is deprecated and will likely be removed in a future release.

主要關注一下紅色框起來的部分。數據結構

http://dl2.iteye.com/upload/attachment/0113/7258/600df987-99ce-3507-af03-1513ff369f0b.png

 對上圖中一條完整收集記錄進行解釋:架構

 http://dl2.iteye.com/upload/attachment/0113/7260/997be6c9-5e65-39a3-8d0c-c771a4c49f4b.png
[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);
3、CMS垃圾收集器總結
當使用CMS收集器時,當開始進行收集時,old代的收集過程以下所示:
一、首先jvm根據-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什麼時間開始垃圾收集;
二、若是設置了-XX:+UseCMSInitiatingOccupancyOnly,那麼只有當old代佔用確實達到了-XX:CMSInitiatingOccupancyFraction參數所設定的比例時纔會觸發cms gc;
三、若是沒有設置-XX:+UseCMSInitiatingOccupancyOnly,那麼系統會根據統計數據自行決定何時觸發cms gc;所以有時會遇到設置了80%比例才cms gc,可是50%時就已經觸發了,就是由於這個參數沒有設置的緣由;
四、當cms gc開始時,首先的階段是CMS-initial-mark,此階段是初始標記階段,是stop the world階段,所以此階段標記的對象只是從root集最直接可達的對象;
     [1 CMS-initial-mark: 256004K(327680K)] 307919K(504640K),,指標記時,old代的已用空間和總空間
五、下一個階段是CMS-concurrent-mark,此階段是和應用線程併發執行的,所謂併發收集器指的就是這個,主要做用是標記可達的對象
       此階段會打印2條日誌:CMS-concurrent-mark-start,CMS-concurrent-mark
六、下一個階段是CMS-concurrent-preclean,此階段主要是進行 一些預清理,由於標記和應用線程是併發執行的,所以會有些對象的狀態在標記後會改變,此階段正是解決這個問題由於以後的Rescan階段也會stop the world,爲了使暫停的時間儘量的小,也須要preclean階段先作一部分工做以節省時間
     此階段會打印2條日誌:CMS-concurrent-preclean-start,CMS-concurrent-preclean
七、下一階段是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
八、再下一個階段是第二個stop the world階段了,即Rescan階段,此階段暫停應用線程,對對象進行從新掃描並標記;
      [YG occupancy: 102400 K (176960 K)],指執行時young代的狀況
      [1 CMS-remark: 476703K(481324K)] ,指執行時old代的狀況
      此外,還打印出了弱引用處理、類卸載等過程的耗時
九、再下一個階段是CMS-concurrent-sweep,進行併發的垃圾清理
十、最後是CMS-concurrent-reset,爲下一次cms gc重置相關數據結構
十一、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)參數
相關文章
相關標籤/搜索