CMS,全稱Concurrent Low Pause Collector,是jdk1.4後期版本開始引入的新gc算法,在jdk5和jdk6中得到了進一步改進,它的主要適合場景是對響應時間的重要性需求大於對吞吐量的要求,能夠承受垃圾回收線程和應用線程共享處理器資源,並且應用中存在比較多的長生命週期的對象的應用。CMS是用於對tenured generation的回收,也就是年老代的回收,目標是儘量減少應用的暫停時間,減少full gc發生的機率,利用和應用程序線程併發的垃圾回收線程來標記清除年老代。在我們的應用中,因爲有緩存的存在,並且對於響應時間也有比較高的要求,因此希望能嘗試使用CMS來替代默認的server型JVM使用的並行收集器,以便獲得更短的垃圾回收的暫停時間,提高程序的響應性。
CMS並非沒有暫停,而是用兩次短暫停來替代串行標記整理算法的長暫停,它的收集週期是這樣:
初始標記(CMS-initial-mark) -> 併發標記(CMS-concurrent-mark) -> 重新標記(CMS-remark) -> 併發清除(CMS-concurrent-sweep) ->併發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)。
其中的1,3兩個步驟需要暫停所有的應用程序線程的。第一次暫停從root對象開始標記存活的對象,這個階段稱爲初始標記;第二次暫停是在併發標記之後,暫停所有應用程序線程,重新標記併發標記階段遺漏的對象(在併發標記階段結束後對象狀態的更新導致)。第一次暫停會比較短,第二次暫停通常會比較長,並且remark這個階段可以並行標記。
而併發標記、併發清除、併發重設階段的所謂併發,是指一個或者多個垃圾回收線程和應用程序線程併發地運行,垃圾回收線程不會暫停應用程序的執行,如果你有多於一個處理器,那麼併發收集線程將與應用線程在不同的處理器上運行,顯然,這樣的開銷就是會降低應用的吞吐量。Remark階段的並行,是指暫停了所有應用程序後,啓動一定數目的垃圾回收進程進行並行標記,此時的應用線程是暫停的。
CMS的young generation的回收採用的仍然是並行複製收集器,這個跟Paralle gc算法是一致的。
下面是參數介紹和遇到的問題總結,
1、啓用CMS:-XX:+UseConcMarkSweepGC。 咳咳,這裏犯過一個低級錯誤,竟然將+號寫成了-號
2。CMS默認啓動的回收線程數目是 (ParallelGCThreads + 3)/4),如果你需要明確設定,可以通過-XX:ParallelCMSThreads=20來設定,其中ParallelGCThreads是年輕代的並行收集線程數
3、CMS是不會整理堆碎片的,因此爲了防止堆碎片引起full gc,通過會開啓CMS階段進行合併碎片選項:-XX:+UseCMSCompactAtFullCollection,開啓這個選項一定程度上會影響性能,阿寶的blog裏說也許可以通過配置適當的CMSFullGCsBeforeCompaction來調整性能,未實踐。
4.爲了減少第二次暫停的時間,開啓並行remark: -XX:+CMSParallelRemarkEnabled,如果remark還是過長的話,可以開啓-XX:+CMSScavengeBeforeRemark選項,強制remark之前開始一次minor gc,減少remark的暫停時間,但是在remark之後也將立即開始又一次minor gc。
5.爲了避免Perm區滿引起的full gc,建議開啓CMS回收Perm區選項:
+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
6.默認CMS是在tenured generation沾滿68%的時候開始進行CMS收集,如果你的年老代增長不是那麼快,並且希望降低CMS次數的話,可以適當調高此值:
-XX:CMSInitiatingOccupancyFraction=80
這裏修改成80%沾滿的時候纔開始CMS回收。
7.年輕代的並行收集線程數默認是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),如果你希望設定這個線程數,可以通過-XX:ParallelGCThreads= N 來調整。
8.進入重點,在初步設置了一些參數後,例如:
-XX:MaxPermSize=64m -XX:-UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSParallelRemarkEnabled
-XX:SoftRefLRUPolicyMSPerMB=0
需要在生產環境或者壓測環境中測量這些參數下系統的表現,這時候需要打開GC日誌查看具體的信息,因此加上參數:
-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log
在運行相當長一段時間內查看CMS的表現情況,CMS的日誌輸出類似這樣:
4391.352: [CMS-concurrent-mark-start]
4391.779: [CMS-concurrent-mark: 0.427/0.427 secs] [Times: user=1.24 sys=0.31, real=0.42 secs]
4391.779: [CMS-concurrent-preclean-start]
4391.821: [CMS-concurrent-preclean: 0.040/0.042 secs] [Times: user=0.13 sys=0.03, real=0.05 secs]
4391.821: [CMS-concurrent-abortable-preclean-start]
4392.511: [CMS-concurrent-abortable-preclean: 0.349/0.690 secs] [Times: user=2.02 sys=0.51, real=0.69 secs]
4392.516: [GC[YG occupancy: 111001 K (235968 K)]4392.516: [Rescan (parallel) , 0.0309960 secs]4392.547: [weak refs processing, 0.0417710 secs] [1 CMS-remark: 655734K(1310720K)] 766736K(1546688K), 0.0932010 secs] [Times: user=0.17 sys=0.00, real=0.09 secs]
4392.609: [CMS-concurrent-sweep-start]
4394.310: [CMS-concurrent-sweep: 1.595/1.701 secs] [Times: user=4.78 sys=1.05, real=1.70 secs]
4394.310: [CMS-concurrent-reset-start]
4394.364: [CMS-concurrent-reset: 0.054/0.054 secs] [Times: user=0.14 sys=0.06, real=0.06 secs]
其中可以看到CMS-initial-mark階段暫停了0.0303050秒,而CMS-remark階段暫停了0.0932010秒,因此兩次暫停的總共時間是0.123506秒,也就是123毫秒左右。兩次短暫停的時間之和在200以下可以稱爲正常現象。
但是你很可能遇到兩種fail引起full gc:Prommotion failed和Concurrent mode failed。
Prommotion failed的日誌輸出大概是這樣:
2166784K), 9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]
這個問題的產生是由於救助空間不夠,從而向年老代轉移對象,年老代沒有足夠的空間來容納這些對象,導致一次full gc的產生。解決這個問題的辦法有兩種完全相反的傾向:增大救助空間、增大年老代或者去掉救助空間。增大救助空間就是調整-XX:SurvivorRatio參數,這個參數是Eden區和Survivor區的大小比值,默認是32,也就是說Eden區是Survivor區的32倍大小,要注意Survivo是有兩個區的,因此Surivivor其實佔整個young genertation的1/34。調小這個參數將增大survivor區,讓對象儘量在survitor區呆長一點,減少進入年老代的對象。去掉救助空間的想法是讓大部分不能馬上回收的數據儘快進入年老代,加快年老代的回收頻率,減少年老代暴漲的可能性,這個是通過將-XX:SurvivorRatio 設置成比較大的值(比如65536)來做到。在我們的應用中,將young generation設置成256M,這個值相對來說比較大了,而救助空間設置成默認大小(1/34),從壓測情況來看,沒有出現prommotion failed的現象,年輕代比較大,從GC日誌來看,minor gc的時間也在5-20毫秒內,還可以接受,因此暫不調整。
Concurrent mode failed的產生是由於CMS回收年老代的速度太慢,導致年老代在CMS完成前就被沾滿,引起full gc,避免這個現象的產生就是調小-XX:CMSInitiatingOccupancyFraction參數的值,讓CMS更早更頻繁的觸發,降低年老代被沾滿的可能。我們的應用暫時負載比較低,在生產環境上年老代的增長非常緩慢,因此暫時設置此參數爲80。在壓測環境下,這個參數的表現還可以,沒有出現過Concurrent mode failed。
在初步確定CMS參數後,系統運行了幾天,今天嘗試在線上打開了GC日誌,按阿寶同學的說法是gc日誌的開銷比之jstat還小,打開之後發現確實影響很小。打開GC日誌之後又發現幾個隱藏的問題比較有價值,這裏記錄下。
首先是系統在啓動的時候有一次System.gc()調用引起的full gc,日誌輸出類似這樣:
這個方法強制調用了System.gc():
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
if (size <= maxMemory - reservedMemory) {
reservedMemory += size;
return;
}
}
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (reservedMemory + size > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
}
}
但是在這次GC後,又發現每隔一個小時就有一次System.gc()引起的full gc,這就太難以忍受了,日誌大概是這樣,注意間隔時間都是3600秒左右:
14171.971: [Full GC (System) 14171.971: [CMS: 680799K->83681K(1310720K), 1.0171580 secs] 836740K->83681K(1546688K), [CMS Perm : 20215K->15599K(65536K)], 1.0173850 secs] [Times: user=0.97 sys=0.01, real=1.02 secs]
17774.020: [Full GC (System) 17774.020: [CMS: 676201K->79331K(1310720K), 0.9652670 secs] 817596K->79331K(1546688K), [CMS Perm : 22808K->15619K(65536K)], 0.9655150 secs] [Times: user=0.93 sys=0.02, real=0.97 secs]
21374.989: [Full GC (System) 21374.989: [CMS: 677818K->78590K(1310720K), 0.9297080 secs] 822317K->78590K(1546688K), [CMS Perm : 16435K->15593K(65536K)], 0.9299620 secs] [Times: user=0.89 sys=0.01, real=0.93 secs]
24976.948: [Full GC (System) 24976.948: [CMS: 659511K->77608K(1310720K), 0.9255360 secs] 794004K->77608K(1546688K), [CMS Perm : 22359K->15594K(65536K)], 0.9257760 secs] [Times: user=0.88 sys=0.02, real=0.93 secs]
28578.892: [Full GC (System) 28578.892: [CMS: 562058K->77572K(1310720K), 0.8365500 secs] 735072K->77572K(1546688K), [CMS Perm : 15840K->15610K(65536K)], 0.8367990 secs] [Times: user=0.82 sys=0.00, real=0.84 secs]
32179.731: [Full GC (System) 32179.732: [CMS: 549874K->77224K(1310720K), 0.7864400 secs] 561803K->77224K(1546688K), [CMS Perm : 16016K->15597K(65536K)], 0.7866540 secs] [Times: user=0.75 sys=0.01, real=0.79 secs]
搜遍了源碼和依賴庫,沒有再發現顯式的gc調用,問題只能出在運行時上,突然想起我們的系統使用RMI暴露JMX給監控程序,監控程序通過RMI連接JMX監控系統和告警等,會不會是RMI的分佈式垃圾收集導致的?果然,一查資料,RMI的分佈式收集會強制調用System.gc()來進行分佈式GC,server端的間隔恰好是一個小時,這個參數可以通過:
打開GC日誌後發現的另一個問題是remark的時間過長,已經啓用了並行remark,但是時間還是經常超過200毫秒,這個可能的原因有兩個:我們的年老代太大或者觸發CMS的閥值太高了,CMS進行的時候年老代裏的對象已經太多。初步的計劃是調小-XX:SurvivorRatio增大救助空間並且降低-XX:CMSInitiatingOccupancyFraction這個閥值。此外,還找到另一個可選參數-XX:+CMSScavengeBeforeRemark,啓用這個選項後,強制remark之前開始一次minor gc,減少remark的暫停時間,但是在remark之後也將立即開始又一次相對較長時間minor gc,如果你的minor gc很快的話可以考慮下這個選項,暫未實驗。
本以爲在上篇定稿的參數後應該能有比較好的表現,然後實際的表現大出我的意料,cms回收觸發非常頻繁,雖然每次都只是10-50毫秒,但是次數12個小時內能達到180多次,這顯然不正常。通過gc日誌和jstat可以看到,每次old區還在5%左右就開始進行CMS,此時的perm區也才30%,這兩個數字有浮動並且CMS觸發的時間上也沒有規律,在測試環境和生產環境中都是如此。
那麼最後是怎麼解決的呢?其實沒有解決。我只是替換了一個參數就沒再發生這個現象,上文提到爲了避免System.gc()調用引起的full gc,使用了jdk6引入的新參數-XX:+ExplicitGCInvokesConcurrent來讓System.gc()併發執行,但是測試表明恰恰是這個參數引起了CMS的頻繁發生,去掉這個參數就沒有那個奇特的現象。重複檢查了代碼,並且再次查看了GC日誌,沒有再發現有System.gc()的調用,我暫時將原因歸結於使用了ExplicitGCInvokesConcurrent參數後其他方法觸發了CMS,如果有知曉的朋友請留言告知,最後的方案還是徹底禁掉了顯式GC調用。最終定稿的參數:
-server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m
-XX:PermSize=64m -XX:MaxPermSize=64m -XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70
-XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0
-XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC
-XX:SurvivorRatio=8
刪除了+CMSPermGenSweepingEnabled,這個參數在jdk6上跟-XX:+CMSClassUnloadingEnabled作用重疊了,如果你還跑在jdk5上面,那麼應該使用這個參數。救助空間設置爲NewSize的1/10,也就是25M左右,讓年輕代儘量回收,防止年輕對象跑到年老代過早觸發CMS甚至full
gc。CMS的觸發閥值下降到70%,因爲年老代增長較慢,寧願回收次數多一點,降低長暫停的可能。
24小時內的某臺生產機器的表現,通過jstat觀察:
39.70 0.00 5.59 15.15 28.99 20260 326.041 14 0.592 326.633
39.70 0.00 65.49 15.15 28.99 20260 326.041 14 0.592 326.633
0.00 36.93 19.37 15.16 29.01 20261 326.059 14 0.592 326.650
0.00 36.93 93.23 15.16 29.01 20261 326.059 14 0.592 326.650
34.04 0.00 59.62 15.17 29.01 20262 326.076 14 0.592 326.668
0.00 38.55 12.76 15.19 29.01 20263 326.094 14 0.592 326.686
0.00 38.55 65.48 15.19 29.01 20263 326.094 14 0.592 326.686
CMS兩次暫停時間總和在100ms以下,minor gc平均一次執行花了16ms,平均3-4秒發生一次。暫時來看還不錯,也許還可以適當調小一下NewSize,加快以下minor gc。
此次調整總共花了大概一週多的時間,由於經驗不足,還是走了不少彎路,幸好最終的結果還可以,也讓自己對cms gc有比較深入的瞭解。我們的系統在周4晚上已經全部更新上線,從內部測試、壓測、日常測試、beta測試以來,每個階段都發現幾個隱蔽的問題,在上線後暫時沒有再發現問題,證明這個流程還是很有意義的,我過去對流程充滿偏見,現在看來是可笑的。總結我在淘寶5個月越來學習到的東西,幾個關鍵詞:認真、負責、細心、快樂。
參考資料:
《JDK5.0垃圾收集優化之--Don't Pause》 by 江南白衣
《記一次Java GC調整經歷》1,2 by Arbow
Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
Tuning Garbage Collectionwith the 5.0 JavaTM Virtual Machine