一次CMS GC问题排查过程(理解原理+读懂GC日志)
這個是之前處理過的一個線上問題,處理過程斷斷續續,經歷了兩周多的時間,中間各種嘗試,總結如下。這篇文章分三部分:
1、問題的場景和處理過程;2、GC的一些理論東西;3、看懂GC的日志
先說一下問題吧
問題場景:線上機器在半夜會推送一個700M左右的數據,這個時候有個數據置換的過程,也就是說有700M*2的數據在heap區域中,線上系統超時比較多,導致了很嚴重(嚴重程度就不說了)的問題。
問題原因:看日志,系統接口超時的時候,系統出現了FullGC,這個時候stop-the-world了,也就停機了。分析gc的日志,發現有promotion failed,根據FullGC觸發的條件,這個時候就會出現FullGC了。日志如下:
| 12 | 2013-11-27T03:00:53.638+0800:?35333.562: [GC?35333.562: [ParNew (promotion failed): 1877376K->1877376K(1877376K),?15.7989680?secs]35349.361: [CMS: 2144171K->2129287K(2146304K),?10.4200280?secs] 3514052K->2129287K(4023680K), [CMS Perm : 119979K->118652K(190132K)],?26.2193500?secs] [Times: user=30.35?sys=5.19, real=26.22?secs] |
問題解決:中間調整過幾次,先搞了幾臺機器做了驗證,后來逐步推廣的。
1、調大heap區,由原來的4g,調整到5g,young區的大小不變,還是2g,這時候old區就由2g變為3g了(這樣保證old區有足夠的空間);
2、設置-XX:UseCMSInitiatingOccupancyOnly,其實這個不關這個問題,只是發現半夜CMS進行的有點頻繁,就禁止掉了悲觀策略;
3、設置CMS區回收的比例,從80%調整到75%,讓old區盡早的進行,有足夠的空間剩余;
?
為什么要有GC(垃圾回收)?
JVM通過GC來回收堆和方法區中的內存,GC的基本原理就是找到程序中不再被使用的對象,然后回收掉這些對象占用的內存。
?
主要的收集器有哪些?
引用計數器和跟蹤計數器兩種。
引用計數器記錄對象是否被引用,當計數器為零時,說明對象已經不再被使用,可以進行回收。java中的對象有復雜的引用關系,不是很適合引用計數器,所以sun jdk中并沒有實現這種GC方式。
跟蹤收集器,全局記錄數據的引用狀態,基于一定的條件觸發。執行的時候,從根集合開始掃描對象的引用關系,主要有復制(copying)、標記-清除(Mark-Sweep)、標記-壓縮(Mark-Compact)那種算法。
?
跟蹤計數器的三種算法簡介?
復制:從根集合搜掃描出存活的對象,然后將存活的對象復制到一塊新的未使用的空間中,當要回收的空間中存活的對象較少時,比較高效;
標記清除:從根集合開始掃描,對存活的對象進行標記,比較完畢后,再掃描整個空間中未標記的對象,然后進行回收,不需要對對象進行移動;
標記壓縮:標記形式和“標記清除”一樣,但是回收不存活的對象后,會把所有存活的對象在內存空間中進行移動,好處是減少了內存碎片,缺點是成本比較高;
?
java內存區域的形式是啥樣的?
這里就不再介紹了,之前有一篇文章中專門介紹這個的(http://iamzhongyong.iteye.com/blog/1333100)。
?
新生代可用的GC?
新生代中對象存活的時間比較短,因此給予Copying算法實現,Eden區域存放新創建的對象,S0和S1區其中一塊用于存放在Minor GC的時候作為復制存活對象的目標空間,另外一塊清空。
串行GC(Serial GC)比較適合單CPU的情況,可以通過-XX:UseSerialGC來強行制定;
并行回收GC(Parallel Scavenge),啟動的時候按照設置的參數來劃定Eden/S0/S1區域的大小,但是在運行時,會根據Minor GC的頻率、消耗時間來動態調整三個區域的大小,可以用過-XX:UseAdaptiveSizePolicy來固定大小,不進行動態調整;
并行GC(ParNew)劃分Eden、S1、S0的區域上和串行GC一樣。并行GC需要配合舊生代使用CMS GC(這是他和并行回收GC的不同)(如果配置了CMS GC的方式,那么新生代默認采取的就是并行GC的方式);
?
啥時候會觸發Minor GC?
當Eden區域分配內存時,發現空間不足,JVM就會觸發Minor GC,程序中System.gc()也可以來觸發。
?
舊生代可用的GC方式有哪幾種?
串行GC(Serial MSC)、并行GC(Parallel MSC)、并發GC(CMS);
?
關于CMS?
采用CMS時候,新生代必須使用Serial GC或者ParNew GC兩種。CMS共有七個步驟,只有Initial Marking和Final Marking兩個階段是stop-the-world的,其他步驟均和應用并行進行。持久代的GC也采用CMS,通過-XX:CMSPermGenSweepingEnabled -XX:CMSClassUnloadingEnabled來制定。在采用cms gc的情況下,ygc變慢的原因通常是由于old gen出現了大量的碎片。
?
為啥CMS會有內存碎片,如何避免?
由于在CMS的回收步驟中,沒有對內存進行壓縮,所以會有內存碎片出現,CMS提供了一個整理碎片的功能,通過-XX:UseCompactAtFullCollection來啟動此功能,啟動這個功能后,默認每次執行Full GC的時候會進行整理(也可以通過-XX:CMSFullGCsBeforeCompaction=n來制定多少次Full GC之后來執行整理),整理碎片會stop-the-world.
?
啥時候會觸發CMS GC?
1、舊生代或者持久代已經使用的空間達到設定的百分比時(CMSInitiatingOccupancyFraction這個設置old區,perm區也可以設置);
2、JVM自動觸發(JVM的動態策略,也就是悲觀策略)(基于之前GC的頻率以及舊生代的增長趨勢來評估決定什么時候開始執行),如果不希望JVM自行決定,可以通過-XX:UseCMSInitiatingOccupancyOnly=true來制定;
3、設置了?-XX:CMSClassUnloadingE考慮nabled 這個則考慮Perm區;
?
啥時候會觸發Full GC?
一、舊生代空間不足:java.lang.outOfMemoryError:java heap space;
二、Perm空間滿:java.lang.outOfMemoryError:PermGen space;
三、CMS GC時出現promotion failed ?和concurrent ?mode failure(Concurrent mode failure發生的原因一般是CMS正在進行,但是由于old區內存不足,需要盡快回收old區里面的死的java對象,這個時候foreground gc需要被觸發,停止所有的java線程,同時終止CMS,直接進行MSC。);
四、統計得到的minor GC晉升到舊生代的平均大小大于舊生代的剩余空間;
五、主動觸發Full GC(執行jmap -histo:live [pid])來避免碎片問題;
?
為啥heap小于3g不建議使用CMS GC這種方式?
http://hellojava.info/?p=142?畢大師的這篇文章講的很清楚。
1、觸發比例不好設置,設置大了,那么剩余的空間就少了很多,設置小了,那old區還沒放置多少東西,就要進行回收了;
2、CMS進行的時候,是并行的,也就意味著如果過于頻繁的話,會和應用的強占CPU;
3、CMS會有內存 碎片問題;
4、YGC的速率變慢(由于CMS GC的實現原理,導致對象從新生代晉升到舊生代時,尋找哪里能放下的這個步驟比ParallelOld GC是慢一些的,因此就導致了YGC速度會有一定程度的下降。);
?
JVM的悲觀策略是啥?
所謂的悲觀策略(http://tmalltesting.com/archives/663?我們性能測試團隊一個同學分析的案例),就是JVM不按照JVM指定的參數來進行CMS GC,而是根據內存情況以及之前回收的方式動態調整,自行進行GC。舊生代剩余的空間(available)大于新生代中使用的空間(max_promotion_in_bytes),或者大于之前平均晉升的old的大小(av_promo),返回false。cms gc是每隔一個周期(默認2s)就會做一次這個檢查,如果為false,則不執行YGC,而觸發cms gc。
?
我們經常使用的是啥GC方式?
針對目前線上機器的情況(8G的物流內存),heap區一般設置在4g或者5g左右,一般是使用CMS GC,這時候:
young區使用ParNew(并行GC),Old+Perm(需要單獨設置)使用CMS,整個堆(young+old+perm)使用MSC((Mark Sweep Compact)是CMS GC算法的Full GC算法,單線程回收整個堆,回收過程有嚴格的步驟。壓縮,所以回收完理論上任何Generation都不會有內存碎片)壓縮回收的方式。
?
讀懂GC日志?
基本上都是這種格式:回收前區域占用的大小->回收后區域占用的大小(區域設置的大小),占用的時間
?
1、promotion failed的一段日志
| 12 | 2013-11-27T03:00:53.638+0800:?35333.562: [GC?35333.562: [ParNew (promotion failed): 1877376K->1877376K(1877376K),?15.7989680?secs]35349.361: [CMS: 2144171K->2129287K(2146304K),?10.4200280?secs] 3514052K->2129287K(4023680K), [CMS Perm : 119979K->118652K(190132K)],?26.2193500?secs] [Times: user=30.35?sys=5.19, real=26.22?secs] |
解釋如下:
| 12345 | 1877376K->1877376K(1877376K),?15.7989680?secs?? young區2144171K->2129287K(2146304K),?10.4200280?sec???? old區情況3514052K->2129287K(4023680K)???????????????????? heap區情況119979K->118652K(190132K)],?26.2193500?secs????? perm區情況?[Times: user=30.35?sys=5.19, real=26.22?secs]??? 整個過程的時間消耗 |
?
2、一段正常的CMS的日志
| 1234567891011121314 | 2013-11-27T04:00:12.819+0800:?38892.743: [GC [1?CMS-initial-mark: 1547313K(2146304K)] 1734957K(4023680K),?0.1390860?secs] [Times: user=0.14?sys=0.00, real=0.14?secs]2013-11-27T04:00:12.958+0800:?38892.883: [CMS-concurrent-mark-start]2013-11-27T04:00:19.231+0800:?38899.155: [CMS-concurrent-mark:?6.255/6.272?secs] [Times: user=8.49?sys=1.57, real=6.27?secs]2013-11-27T04:00:19.231+0800:?38899.155: [CMS-concurrent-preclean-start]2013-11-27T04:00:19.250+0800:?38899.175: [CMS-concurrent-preclean:?0.018/0.019?secs] [Times: user=0.02?sys=0.00, real=0.02?secs]2013-11-27T04:00:19.250+0800:?38899.175: [CMS-concurrent-abortable-preclean-start]?CMS: abort preclean due to time?2013-11-27T04:00:25.252+0800:?38905.176: [CMS-concurrent-abortable-preclean:?5.993/6.002?secs] [Times: user=6.97?sys=2.16, real=6.00?secs]2013-11-27T04:00:25.253+0800:?38905.177: [GC[YG occupancy:?573705?K (1877376?K)]38905.177: [Rescan (parallel) ,?0.3685690?secs]38905.546: [weak refs processing,?0.0024100?secs]38905.548: [class unloading,?0.0177600?secs]38905.566: [scrub symbol & string tables,?0.0154090?secs] [1?CMS-remark: 1547313K(2146304K)] 2121018K(4023680K),?0.4229380?secs] [Times: user=1.41?sys=0.01, real=0.43?secs]2013-11-27T04:00:25.676+0800:?38905.601: [CMS-concurrent-sweep-start]2013-11-27T04:00:26.436+0800:?38906.360: [CMS-concurrent-sweep:?0.759/0.760?secs] [Times: user=1.06?sys=0.48, real=0.76?secs]2013-11-27T04:00:26.436+0800:?38906.360: [CMS-concurrent-reset-start]2013-11-27T04:00:26.441+0800:?38906.365: [CMS-concurrent-reset:?0.005/0.005?secs] [Times: user=0.00?sys=0.00, real=0.00?secs] |
這個是一個正常的CMS的日志,共分為七個步驟,重點關注initial-mark和remark這兩個階段,因為這兩個是停機的。
A、[GC [1 CMS-initial-mark: 1547313K(2146304K)] 1734957K(4023680K), 0.1390860 secs] [Times: user=0.14 sys=0.00, real=0.14 secs]
各個數據依次表示標記前后old區的所有對象占內存大小和old的capacity,整個JavaHeap(不包括perm)所有對象占內存總的大小和JavaHeap的capacity。
B、2013-11-27T04:00:25.253+0800: 38905.177: [GC[YG occupancy: 573705 K (1877376 K)]38905.177: [Rescan (parallel) , 0.3685690 secs]38905.546: [weak refs processing, 0.0024100 secs]38905.548: [class unloading, 0.0177600 secs]38905.566: [scrub symbol & string tables, 0.0154090 secs] [1 CMS-remark: 1547313K(2146304K)] 2121018K(4023680K), 0.4229380 secs] [Times: user=1.41 sys=0.01, real=0.43 secs]
Rescan (parallel)表示的是多線程處理young區和多線程掃描old+perm的卡表的總時間, parallel 表示多GC線程并行。
weak refs processing 處理old區的弱引用的總時間,用于回收native memory。
class unloading 回收SystemDictionary消耗的總時間。
?
3、一段正常的Young GC的日志
| 12 | 2013-11-27T04:00:07.345+0800:?38887.270: [GC?38887.270: [ParNew: 1791076K->170624K(1877376K),?0.2324440?secs] 2988366K->1413629K(4023680K),?0.2326470?secs] [Times: user=0.80?sys=0.00, real=0.23?secs] |
ParNew這個表明是并行的回收方式,具體的分別是young區、整個heap區的情況;
?
4、一段通過system.gc產生的FullGC日志
| 1 | 2013-07-21T17:44:01.554+0800:?50.568: [Full GC (System)?50.568: [CMS: 943772K->220K(2596864K),?2.3424070?secs] 1477000K->220K(4061184K), [CMS Perm : 3361K->3361K(98304K)],?2.3425410?secs] [Times: user=2.33?sys=0.01, real=2.34?secs] |
解釋如下:
Full GC (System)意味著這是個system.gc調用產生的MSC。
“943772K->220K(2596864K), 2.3424070 secs”表示:這次MSC前后old區內總對象大小,old的capacity及這次MSC耗時。
“1477000K->220K(4061184K)”表示:這次MSC前后JavaHeap內總對象大小,JavaHeap的capacity。
“3361K->3361K(98304K)], 2.3425410 secs”表示:這次MSC前后Perm區內總對象大小,Perm區的capacity。
?
5、一個特殊的GC日志,根據動態計算直接進行的FullGC(MSC的方式)
| 1 | 2013-03-13T13:48:06.349+0800:?7.092: [GC?7.092: [ParNew: 471872K->471872K(471872K),?0.0000420?secs]7.092: [CMS: 366666K->524287K(524288K),?27.0023450?secs] 838538K->829914K(996160K), [CMS Perm : 3196K->3195K(131072K)],?27.0025170?secs] |
ParNew的時間特別短,jvm在minor gc前會首先確認old是不是足夠大,如果不夠大,這次young gc直接返回,進行MSC。
?
參考文章:
http://kenwublog.com/docs/java6-jvm-options-chinese-edition.htm? ?JVM參數大全
http://tmalltesting.com/archives/663?悲觀策略
http://hellojava.info/?p=142?CMS GC不建議3G的原因
畢玄的《分布式java基礎》
如何讀懂GC日志:
https://blogs.oracle.com/poonam/entry/understanding_cms_gc_logs
總結
以上是生活随笔為你收集整理的一次CMS GC问题排查过程(理解原理+读懂GC日志)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python + wordcloud +
- 下一篇: JVM调优——之CMS GC日志分析