原文地址:https://mp.weixin.qq.com/s/fP--JJnkTR92NWdZtdEgqQ
作者:阿飛的博客

對(duì)于許多企業(yè)級(jí)應(yīng)用,尤其是OLTP應(yīng)用來(lái)說(shuō),長(zhǎng)暫停很可能導(dǎo)致服務(wù)超時(shí),而對(duì)這些運(yùn)行在JVM上的應(yīng)用來(lái)說(shuō),垃圾回收(GC)可能是長(zhǎng)暫停最主要的原因。本文將描述一些可能碰到GC長(zhǎng)暫停的不同場(chǎng)景,以及說(shuō)明我們?nèi)绾闻挪楹徒鉀Q這些GC停頓的問(wèn)題。
下面是一些應(yīng)用在運(yùn)行時(shí),可能導(dǎo)致GC長(zhǎng)暫停的不同場(chǎng)景。
1. 碎片化
這個(gè)絕對(duì)要排在第一位。因?yàn)椋且驗(yàn)樗槠瘑?wèn)題--CMS最致命的缺陷,導(dǎo)致這個(gè)統(tǒng)治了OLAP系統(tǒng)十多年的垃圾回收器直接退出歷史舞臺(tái)(CMS已經(jīng)是deprecated,未來(lái)版本會(huì)被移除,請(qǐng)珍惜那些配置了CMS的JVM吧),面對(duì)G1以及最新的ZGC,天生殘(碎)缺(片)的CMS毫無(wú)還手之力。
對(duì)于CMS,由于老年代的碎片化問(wèn)題,在YGC時(shí)可能碰到晉升失敗(promotion failures,即使老年代還有足夠多有效的空間,但是仍然可能導(dǎo)致分配失敗,因?yàn)?strong>沒(méi)有足夠連續(xù)的空間),從而觸發(fā)Concurrent Mode Failure,發(fā)生會(huì)完全STW的FullGC。FullGC相比CMS這種并發(fā)模式的GC需要更長(zhǎng)的停頓時(shí)間才能完成垃圾回收工作,這絕對(duì)是JAVA應(yīng)用最大的災(zāi)難之一。
為什么CMS場(chǎng)景下會(huì)有碎片化問(wèn)題?由于CMS在老年代回收時(shí),采用的是標(biāo)記清理(Mark-Sweep)算法,它在垃圾回收時(shí)并不會(huì)壓縮堆,日積月累,導(dǎo)致老年代的碎片化問(wèn)題會(huì)越來(lái)越嚴(yán)重,直到發(fā)生單線程的Mark-Sweep-Compact GC,即FullGC,會(huì)完全STW。如果堆比較大的話,STW的時(shí)間可能需要好幾秒,甚至十多秒,幾十秒都有可能。
接下來(lái)的cms gc日志,由于碎片率非常高,從而導(dǎo)致promotion failure,然后發(fā)生concurrent mode failure,觸發(fā)的FullGC總計(jì)花了17.1365396秒才完成:



2. GC時(shí)操作系統(tǒng)的活動(dòng)
當(dāng)發(fā)生GC時(shí),一些操作系統(tǒng)的活動(dòng),比如swap,可能導(dǎo)致GC停頓時(shí)間更長(zhǎng),這些停頓可能是幾秒,甚至幾十秒級(jí)別。
如果你的系統(tǒng)配置了允許使用swap空間,操作系統(tǒng)可能把JVM進(jìn)程的非活動(dòng)內(nèi)存頁(yè)移到swap空間,從而釋放內(nèi)存給當(dāng)前活動(dòng)進(jìn)程(可能是操作系統(tǒng)上其他進(jìn)程,取決于系統(tǒng)調(diào)度)。SwApping由于需要訪問(wèn)磁盤,所以相比物理內(nèi)存,它的速度慢的令人發(fā)指。所以,如果在GC的時(shí)候,系統(tǒng)正好需要執(zhí)行Swapping,那么GC停頓的時(shí)間一定會(huì)非常非常非常恐怖。
下面是一段持續(xù)了29.48秒的YGC日志:

最后一行[Times: user=915.56, sys=6.35, real=29.48 secs]中real就是YGC時(shí)應(yīng)用真實(shí)的停頓時(shí)間。
發(fā)生YGC的這個(gè)時(shí)間點(diǎn),vmstat命令輸出結(jié)果如下:

YGC總計(jì)花了29秒才完成。vmstat命令輸出結(jié)果表示,可用swap空間在這個(gè)時(shí)間段減少了600m。這就意味著,在GC的時(shí)候,內(nèi)存中的一些頁(yè)被移到了swap空間,這個(gè)內(nèi)存頁(yè)不一定屬于JVM進(jìn)程,可能是其他操作系統(tǒng)上的其他進(jìn)程。
從上面可以看出,操作系統(tǒng)上可用物理內(nèi)容不足以運(yùn)行系統(tǒng)上所有的進(jìn)程,解決辦法就是盡可能運(yùn)行更少的進(jìn)程,增加RAM從而提升系統(tǒng)的物理內(nèi)存。在這個(gè)例子中,Old區(qū)有9G,但是只使用了1.8G(mark-sweep generation total 9437184K, used 1860619K)。我們可以適當(dāng)?shù)慕档蚈ld區(qū)的大小以及整個(gè)堆的大小,從而減少內(nèi)存壓力,最小化系統(tǒng)上的應(yīng)用發(fā)生swapping的可能。
除了swapping以外,我們也需要監(jiān)控了解長(zhǎng)GC暫停時(shí)的任何IO或者網(wǎng)絡(luò)活動(dòng)情況等, 可以通過(guò)IOStat和netstat兩個(gè)工具來(lái)實(shí)現(xiàn). 我們還能通過(guò)mpstat查看CPU統(tǒng)計(jì)信息,從而弄清楚在GC的時(shí)候是否有足夠的CPU資源。
3. 堆空間不夠
如果應(yīng)用程序需要的內(nèi)存比我們執(zhí)行的Xmx還要大,也會(huì)導(dǎo)致頻繁的垃圾回收,甚至OOM。由于堆空間不足,對(duì)象分配失敗,JVM就需要調(diào)用GC嘗試回收已經(jīng)分配的空間,但是GC并不能釋放更多的空間,從而又回導(dǎo)致GC,進(jìn)入惡性循環(huán)。
應(yīng)用運(yùn)行時(shí),頻繁的FullGC會(huì)引起長(zhǎng)時(shí)間停頓,在下面這個(gè)例子中,Perm空間幾乎是滿的,并且在Perm區(qū)嘗試分配內(nèi)存也都失敗了,從而觸發(fā)FullGC:

同樣的,如果在老年代的空間不夠的話,也會(huì)導(dǎo)致頻繁FullGC,這類問(wèn)題比較好辦,給足老年代和永久代,不要做太摳門的人了,嘿嘿。
4. JVM Bug
什么軟件都有BUG,JVM也不例外。有時(shí)候,GC的長(zhǎng)時(shí)間停頓就有可能是BUG引起的。例如,下面列舉的這些JVM的BUG,就可能導(dǎo)致Java應(yīng)用在GC時(shí)長(zhǎng)時(shí)間停頓。

如果你的JDK正好是上面這些版本,強(qiáng)烈建議升級(jí)到更新BUG已經(jīng)修復(fù)的版本。
5. 顯示System.gc調(diào)用
檢查是否有顯示的System.gc調(diào)用,應(yīng)用中的一些類里,或者第三方模塊中調(diào)用System.gc調(diào)用從而觸發(fā)STW的FullGC,也可能會(huì)引起非常長(zhǎng)時(shí)間的停頓。如下GC日志所示,F(xiàn)ull GC后面的(System)表示它是由調(diào)用System.GC觸發(fā)的FullGC,并且耗時(shí)5.75秒:

如果你使用了RMI,能觀察到固定時(shí)間間隔的FullGC,也是由于RMI的實(shí)現(xiàn)調(diào)用了System.gc。這個(gè)時(shí)間間隔可以通過(guò)系統(tǒng)屬性配置:

JDK 1.4.2和5.0的默認(rèn)值是60000毫秒,即1分鐘;JDK6以及以后的版本,默認(rèn)值是3600000毫秒,即1個(gè)小時(shí)。
如果你要關(guān)閉通過(guò)調(diào)用System.gc()觸發(fā)FullGC,配置JVM參數(shù) -XX:+DisableExplicitGC即可。
那么如何定位并解決這類問(wèn)題問(wèn)題呢?
- 配置JVM參數(shù):-XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps and -XX:+PrintGCApplicationStoppedTime. 如果是CMS,還需要添加-XX:PrintFLSStatistics=2,然后收集GC日志。因?yàn)镚C日志能告訴我們GC頻率,是否長(zhǎng)時(shí)間停頓等重要信息。
- 使用vmstat, iostat, netstat和mpstat等工具監(jiān)控系統(tǒng)全方位健康狀況。
- 使用GCHisto工具可視化分析GC日志,弄明白消耗了很長(zhǎng)時(shí)間的GC,以及這些GC的出現(xiàn)是否有一定的規(guī)律。
- 嘗試從GC日志中能否找出一下JVM堆碎片化的表征。
- 監(jiān)控指定應(yīng)用的堆大小是否足夠。
- 檢查你運(yùn)行的JVM版本,是否有與長(zhǎng)時(shí)間停頓相關(guān)的BUG,然后升級(jí)到修復(fù)問(wèn)題的最新JDK。