線上服務(wù)的FGC問題排查,看這篇就夠了!
線上服務(wù)的GC問題,是Java程序非常典型的一類問題,非??简?yàn)工程師排查問題的能力。同時,幾乎是面試必考題,但是能真正答好此題的人并不多,要么原理沒吃透,要么缺乏實(shí)戰(zhàn)經(jīng)驗(yàn)。
過去半年時間里,我們的廣告系統(tǒng)出現(xiàn)了多次和GC相關(guān)的線上問題,有Full GC過于頻繁的,有Young GC耗時過長的,這些問題帶來的影響是:GC過程中的程序卡頓,進(jìn)一步導(dǎo)致服務(wù)超時從而影響到廣告收入。
這篇文章,我將以一個FGC頻繁的線上案例作為引子,詳細(xì)介紹下GC的排查過程,另外會結(jié)合GC的運(yùn)行原理給出一份實(shí)踐指南,希望對你有所幫助。內(nèi)容分成以下3個部分:
-
從一次FGC頻繁的線上案例說起 -
GC的運(yùn)行原理介紹 -
排查FGC問題的實(shí)踐指南
去年10月份,我們的廣告召回系統(tǒng)在程序上線后收到了FGC頻繁的系統(tǒng)告警,通過下面的監(jiān)控圖可以看到:平均每35分鐘就進(jìn)行了一次FGC。而程序上線前,我們的FGC頻次大概是2天一次。下面,詳細(xì)介紹下該問題的排查過程。
-Xms4g -Xmx4g -Xmn2g -Xss1024K
-XX:ParallelGCThreads=5
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80
通過觀察老年代的使用情況,可以看到:每次FGC后,內(nèi)存都能回到500M左右,因此我們排除了內(nèi)存泄漏的情況。
上圖中,按照對象所占內(nèi)存大小排序,顯示了存活對象的實(shí)例數(shù)、所占內(nèi)存、類名??梢钥吹脚琶谝坏氖牵?/span>int[],而且所占內(nèi)存大小遠(yuǎn)遠(yuǎn)超過其他存活對象。至此,我們將懷疑目標(biāo)鎖定在了 int[] .
鎖定 int[] 后,我們打算dump堆內(nèi)存文件,通過可視化工具進(jìn)一步跟蹤對象的來源。考慮堆轉(zhuǎn)儲過程中會暫停程序,因此我們先從服務(wù)管理平臺摘掉了此節(jié)點(diǎn),然后通過以下命令dump堆內(nèi)存:
jmap -dump:format=b,file=heap 7276
通過JVisualVM工具導(dǎo)入dump出來的堆內(nèi)存文件,同樣可以看到各個對象所占空間,其中int[]占到了50%以上的內(nèi)存,進(jìn)一步往下便可以找到 int[] 所屬的業(yè)務(wù)對象,發(fā)現(xiàn)它來自于架構(gòu)團(tuán)隊(duì)提供的codis基礎(chǔ)組件。
5. 通過代碼分析可疑對象
通過代碼分析,codis基礎(chǔ)組件每分鐘會生成約40M大小的int數(shù)組,用于統(tǒng)計(jì)TP99 和 TP90,數(shù)組的生命周期是一分鐘。而根據(jù)第2步觀察老年代的內(nèi)存變化時,發(fā)現(xiàn)老年代的內(nèi)存基本上也是每分鐘增加40多M,因此推斷:這40M的int數(shù)組應(yīng)該是從新生代晉升到老年代。
我們進(jìn)一步查看了YGC的頻次監(jiān)控,通過下圖可以看到大概1分鐘有8次左右的YGC,這樣基本驗(yàn)證了我們的推斷:因?yàn)镃MS收集器默認(rèn)的分代年齡是6次,即YGC 6次后還存活的對象就會晉升到老年代,而codis組件中的大數(shù)組生命周期是1分鐘,剛好滿足這個要求。
6. 解決方案
為了快速解決問題,我們將CMS收集器的分代年齡改成了15次,改完后FGC頻次恢復(fù)到了2天一次,后續(xù)如果YGC的頻次超過每分鐘15次還會再次觸發(fā)此問題。當(dāng)然,我們最根本的解決方案是:優(yōu)化程序以降低YGC的頻率,同時縮短codis組件中int數(shù)組的生命周期,這里就不做展開了。
上面整個案例的分析過程中,其實(shí)涉及到很多GC的原理知識,如果不懂得這些原理就著手處理,其實(shí)整個排查過程是很抓瞎的。
大家都知道: GC分為YGC和FGC,它們均發(fā)生在JVM的堆內(nèi)存上。先來看下JDK8的堆內(nèi)存結(jié)構(gòu):
下面4種情況,對象會進(jìn)入到老年代中:
-
YGC時,To Survivor區(qū)不足以存放存活的對象,對象會直接進(jìn)入到老年代。 -
經(jīng)過多次YGC后,如果存活對象的年齡達(dá)到了設(shè)定閾值,則會晉升到老年代中。 -
動態(tài)年齡判定規(guī)則,To Survivor區(qū)中相同年齡的對象,如果其大小之和占到了 To Survivor區(qū)一半以上的空間,那么大于此年齡的對象會直接進(jìn)入老年代,而不需要達(dá)到默認(rèn)的分代年齡。 -
大對象:由-XX:PretenureSizeThreshold啟動參數(shù)控制, 若對象大小大于此值,就會繞過新生代, 直接在老年代中分配。
當(dāng)晉升到老年代的對象大于了老年代的剩余空間時,就會觸發(fā)FGC(Major GC),FGC處理的區(qū)域同時包括新生代和老年代。除此之外,還有以下4種情況也會觸發(fā)FGC:
老年代的內(nèi)存使用率達(dá)到了一定閾值(可通過參數(shù)調(diào)整),直接觸發(fā)FGC。
-
空間分配擔(dān)保:在YGC之前,會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間。如果小于,說明YGC是不安全的,則會查看參數(shù) HandlePromotionFailure 是否被設(shè)置成了允許擔(dān)保失敗,如果不允許則直接觸發(fā)Full GC;如果允許,那么會進(jìn)一步檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果小于也會觸發(fā) Full GC。 -
Metaspace(元空間)在空間不足時會進(jìn)行擴(kuò)容,當(dāng)擴(kuò)容到了-XX:MetaspaceSize 參數(shù)的指定值時,也會觸發(fā)FGC。 -
System.gc() 或者Runtime.gc() 被顯式調(diào)用時,觸發(fā)FGC。
不管YGC還是FGC,都會造成一定程度的程序卡頓(即Stop The World問題:GC線程開始工作,其他工作線程被掛起),即使采用ParNew、CMS或者G1這些更先進(jìn)的垃圾回收算法,也只是在減少卡頓時間,而并不能完全消除卡頓。
那到底什么情況下,GC會對程序產(chǎn)生影響呢?根據(jù)嚴(yán)重程度從高到底,我認(rèn)為包括以下4種情況:
FGC過于頻繁:FGC通常是比較慢的,少則幾百毫秒,多則幾秒,正常情況FGC每隔幾個小時甚至幾天才執(zhí)行一次,對系統(tǒng)的影響還能接受。但是,一旦出現(xiàn)FGC頻繁(比如幾十分鐘就會執(zhí)行一次),這種肯定是存在問題的,它會導(dǎo)致工作線程頻繁被停止,讓系統(tǒng)看起來一直有卡頓現(xiàn)象,也會使得程序的整體性能變差。
-
YGC耗時過長 :一般來說,YGC的總耗時在幾十或者上百毫秒是比較正常的,雖然會引起系統(tǒng)卡頓幾毫秒或者幾十毫秒,這種情況幾乎對用戶無感知,對程序的影響可以忽略不計(jì)。但是如果YGC耗時達(dá)到了1秒甚至幾秒(都快趕上FGC的耗時了),那卡頓時間就會增大,加上YGC本身比較頻繁,就會導(dǎo)致比較多的服務(wù)超時問題。 -
FGC耗時過長 :FGC耗時增加,卡頓時間也會隨之增加,尤其對于高并發(fā)服務(wù),可能導(dǎo)致FGC期間比較多的超時問題,可用性降低,這種也需要關(guān)注。
-
YGC過于頻繁 :即使YGC不會引起服務(wù)超時,但是YGC過于頻繁也會降低服務(wù)的整體性能,對于高并發(fā)服務(wù)也是需要關(guān)注的。
-
大對象:系統(tǒng)一次性加載了過多數(shù)據(jù)到內(nèi)存中(比如SQL查詢未做分頁),導(dǎo)致大對象進(jìn)入了老年代。 -
內(nèi)存泄漏:頻繁創(chuàng)建了大量對象,但是無法被回收(比如IO對象使用完后未調(diào)用close方法釋放資源),先引發(fā)FGC,最后導(dǎo)致OOM. -
程序頻繁生成一些長生命周期的對象,當(dāng)這些對象的存活年齡超過分代年齡時便會進(jìn)入老年代,最后引發(fā)FGC. (即本文中的案例) -
程序BUG導(dǎo)致動態(tài)生成了很多新類,使得 Metaspace 不斷被占用,先引發(fā)FGC,最后導(dǎo)致OOM. -
代碼中顯式調(diào)用了 gc方法,包括自己的代碼甚至框架中的代碼。 -
JVM參數(shù)設(shè)置問題:包括總內(nèi)存大小、新生代和老年代的大小、Eden區(qū)和S區(qū)的大小、元空間大小、垃圾回收算法等等。
-
公司的監(jiān)控系統(tǒng):大部分公司都會有,可全方位監(jiān)控JVM的各項(xiàng)指標(biāo)。 -
JDK的自帶工具,包括jmap、jstat等常用命令: # 查看 堆內(nèi)存各區(qū)域的使用率 以及GC情況
jstat -gcutil -h20 pid 1000 # 查看堆內(nèi)存中的存活對象,并按空間排序 jmap -histo pid | head -n20 # dump堆內(nèi)存文件 jmap -dump:format=b,file=heap pid -
可視化的堆內(nèi)存分析工具:JVisualVM、MAT等
3. 排查指南
-
查看監(jiān)控,以了解出現(xiàn)問題的時間點(diǎn)以及當(dāng)前FGC的頻率(可對比正常情況看頻率是否正常) -
了解該時間點(diǎn)之前有沒有程序上線、基礎(chǔ)組件升級等情況。 -
了解JVM的參數(shù)設(shè)置,包括:堆空間各個區(qū)域的大小設(shè)置,新生代和老年代分別采用了哪些垃圾收集器,然后分析JVM參數(shù)設(shè)置是否合理。 -
再對步驟1中列出的可能原因做排除法,其中元空間被打滿、內(nèi)存泄漏、代碼顯式調(diào)用gc方法比較容易排查。 -
針對大對象或者長生命周期對象導(dǎo)致的FGC,可通過 jmap -histo 命令并結(jié)合dump堆內(nèi)存文件作進(jìn)一步分析,需要先定位到可疑對象。 -
通過可疑對象定位到具體代碼再次分析,這時候要結(jié)合GC原理和JVM參數(shù)設(shè)置,弄清楚可疑對象是否滿足了進(jìn)入到老年代的條件才能下結(jié)論。
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點(diǎn)個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!