線上服務(wù)的GC問題,是Java程序非常典型的一類問題猜拾,非常考驗(yàn)工程師排查問題的能力佣盒。同時(shí)挎袜,幾乎是面試必考題,但是能真正答好此題的人并不多,要么原理沒吃透盯仪,要么缺乏實(shí)戰(zhàn)經(jīng)驗(yàn)紊搪。
過去半年時(shí)間里,我們的廣告系統(tǒng)出現(xiàn)了多次和GC相關(guān)的線上問題全景,有Full GC過于頻繁的耀石,有Young GC耗時(shí)過長的,這些問題帶來的影響是:GC過程中的程序卡頓爸黄,進(jìn)一步導(dǎo)致服務(wù)超時(shí)從而影響到廣告收入滞伟。
這篇文章,我將以一個(gè)FGC頻繁的線上案例作為引子炕贵,詳細(xì)介紹下GC的排查過程梆奈,另外會(huì)結(jié)合GC的運(yùn)行原理給出一份實(shí)踐指南,希望對(duì)你有所幫助称开。內(nèi)容分成以下3個(gè)部分:
從一次FGC頻繁的線上案例說起
GC的運(yùn)行原理介紹
排查FGC問題的實(shí)踐指南
01 從一次FGC頻繁的線上案例說起
去年10月份亩钟,我們的廣告召回系統(tǒng)在程序上線后收到了FGC頻繁的系統(tǒng)告警,通過下面的監(jiān)控圖可以看到:平均每35分鐘就進(jìn)行了一次FGC鳖轰。而程序上線前清酥,我們的FGC頻次大概是2天一次。下面蕴侣,詳細(xì)介紹下該問題的排查過程焰轻。
- 檢查JVM配置
通過以下命令查看JVM的啟動(dòng)參數(shù):
ps aux | grep "applicationName=adsearch"
-Xms4g -Xmx4g -Xmn2g -Xss1024K
-XX:ParallelGCThreads=5
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80
參數(shù)解析:
-XX:+ ‘+’表示啟用該選項(xiàng)
-XX:- ‘-‘表示關(guān)閉該選項(xiàng)
-Xmx4g:堆內(nèi)存最大值為4GB。
-Xms4g:初始化堆內(nèi)存大小為4GB睛蛛。
-Xmn2G:設(shè)置年輕代大小為2G
-Xss512k:設(shè)置每個(gè)線程的堆棧大小
-XX:ParallelGCThreads=5:新生代并行收集器的線程數(shù)鹦马。
可以看到堆內(nèi)存為4G,新生代為2G忆肾,老年代也為2G荸频,新生代采用ParNew收集器,老年代采用并發(fā)標(biāo)記清除的CMS收集器客冈,當(dāng)老年代的內(nèi)存占用率達(dá)到80%時(shí)會(huì)進(jìn)行FGC旭从。
進(jìn)一步通過 jmap -heap 7276 | head -n20 可以得知新生代的Eden區(qū)為1.6G,S0和S1區(qū)均為0.2G场仲。
2. 觀察老年代的內(nèi)存變化
通過觀察老年代的使用情況和悦,可以看到:每次FGC后,內(nèi)存都能回到500M左右渠缕,因此我們排除了內(nèi)存泄漏的情況鸽素。
3. 通過jmap命令查看堆內(nèi)存中的對(duì)象
通過命令 jmap -histo 7276 | head -n20
上圖中,按照對(duì)象所占內(nèi)存大小排序亦鳞,顯示了存活對(duì)象的實(shí)例數(shù)馍忽、所占內(nèi)存棒坏、類名≡馑瘢可以看到排名第一的是:int[]坝冕,而且所占內(nèi)存大小遠(yuǎn)遠(yuǎn)超過其他存活對(duì)象。至此瓦呼,我們將懷疑目標(biāo)鎖定在了 int[] .
4. 進(jìn)一步dump堆內(nèi)存文件進(jìn)行分析
鎖定 int[] 后喂窟,我們打算dump堆內(nèi)存文件,通過可視化工具進(jìn)一步跟蹤對(duì)象的來源央串∧ピ瑁考慮堆轉(zhuǎn)儲(chǔ)過程中會(huì)暫停程序,因此我們先從服務(wù)管理平臺(tái)摘掉了此節(jié)點(diǎn)蹋辅,然后通過以下命令dump堆內(nèi)存:
jmap -dump:format=b,file=heap 7276
通過JVisualVM工具導(dǎo)入dump出來的堆內(nèi)存文件钱贯,同樣可以看到各個(gè)對(duì)象所占空間,其中int[]占到了50%以上的內(nèi)存侦另,進(jìn)一步往下便可以找到 int[] 所屬的業(yè)務(wù)對(duì)象秩命,發(fā)現(xiàn)它來自于架構(gòu)團(tuán)隊(duì)提供的codis基礎(chǔ)組件。
5. 通過代碼分析可疑對(duì)象
通過代碼分析褒傅,codis基礎(chǔ)組件每分鐘會(huì)生成約40M大小的int數(shù)組弃锐,用于統(tǒng)計(jì)TP99 和 TP90,數(shù)組的生命周期是一分鐘殿托。而根據(jù)第2步觀察老年代的內(nèi)存變化時(shí)霹菊,發(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次后還存活的對(duì)象就會(huì)晉升到老年代饶碘,而codis組件中的大數(shù)組生命周期是1分鐘,剛好滿足這個(gè)要求馒吴。
至此扎运,整個(gè)排查過程基本結(jié)束了,那為什么程序上線前沒出現(xiàn)此問題呢饮戳?通過上圖可以看到:程序上線前YGC的頻次在5次左右豪治,此次上線后YGC頻次變成了8次左右,從而引發(fā)了此問題扯罐。
6. 解決方案
為了快速解決問題负拟,我們將CMS收集器的分代年齡改成了15次,改完后FGC頻次恢復(fù)到了2天一次歹河,后續(xù)如果YGC的頻次超過每分鐘15次還會(huì)再次觸發(fā)此問題齿椅。當(dāng)然琉挖,我們最根本的解決方案是:優(yōu)化程序以降低YGC的頻率,同時(shí)縮短codis組件中int數(shù)組的生命周期涣脚,這里就不做展開了。
02 GC的運(yùn)行原理介紹
上面整個(gè)案例的分析過程中寥茫,其實(shí)涉及到很多GC的原理知識(shí)遣蚀,如果不懂得這些原理就著手處理,其實(shí)整個(gè)排查過程是很抓瞎的纱耻。
這里芭梯,我選擇幾個(gè)最核心的知識(shí)點(diǎn),展開介紹下GC的運(yùn)行原理弄喘,最后再給出一份實(shí)踐指南玖喘。
1. 堆內(nèi)存結(jié)構(gòu)
大家都知道: GC分為YGC和FGC,它們均發(fā)生在JVM的堆內(nèi)存上蘑志。先來看下JDK8的堆內(nèi)存結(jié)構(gòu):
可以看到累奈,堆內(nèi)存采用了分代結(jié)構(gòu),包括新生代和老年代急但。新生代又分為:Eden區(qū)澎媒,F(xiàn)rom Survivor區(qū)(簡稱S0),To Survivor區(qū)(簡稱S1區(qū))波桩,三者的默認(rèn)比例為8:1:1戒努。另外,新生代和老年代的默認(rèn)比例為1:2镐躲。
堆內(nèi)存之所以采用分代結(jié)構(gòu)储玫,是考慮到絕大部分對(duì)象都是短生命周期的,這樣不同生命周期的對(duì)象可放在不同的區(qū)域中萤皂,然后針對(duì)新生代和老年代采用不同的垃圾回收算法撒穷,從而使得GC效率最高。
2. YGC是什么時(shí)候觸發(fā)的敌蚜?
大多數(shù)情況下桥滨,對(duì)象直接在年輕代中的Eden區(qū)進(jìn)行分配,如果Eden區(qū)域沒有足夠的空間弛车,那么就會(huì)觸發(fā)YGC(Minor GC)齐媒,YGC處理的區(qū)域只有新生代。因?yàn)榇蟛糠謱?duì)象在短時(shí)間內(nèi)都是可收回掉的纷跛,因此YGC后只有極少數(shù)的對(duì)象能存活下來喻括,而被移動(dòng)到S0區(qū)(采用的是復(fù)制算法)。
當(dāng)觸發(fā)下一次YGC時(shí)贫奠,會(huì)將Eden區(qū)和S0區(qū)的存活對(duì)象移動(dòng)到S1區(qū)唬血,同時(shí)清空Eden區(qū)和S0區(qū)望蜡。當(dāng)再次觸發(fā)YGC時(shí),這時(shí)候處理的區(qū)域就變成了Eden區(qū)和S1區(qū)(即S0和S1進(jìn)行角色交換)拷恨。每經(jīng)過一次YGC脖律,存活對(duì)象的年齡就會(huì)加1。
3. FGC又是什么時(shí)候觸發(fā)的腕侄?
下面4種情況小泉,對(duì)象會(huì)進(jìn)入到老年代中:
- YGC時(shí),To Survivor區(qū)不足以存放存活的對(duì)象冕杠,對(duì)象會(huì)直接進(jìn)入到老年代微姊。
- 經(jīng)過多次YGC后,如果存活對(duì)象的年齡達(dá)到了設(shè)定閾值分预,則會(huì)晉升到老年代中兢交。
- 動(dòng)態(tài)年齡判定規(guī)則,To Survivor區(qū)中相同年齡的對(duì)象笼痹,如果其大小之和占到了 To Survivor區(qū)一半以上的空間配喳,那么大于此年齡的對(duì)象會(huì)直接進(jìn)入老年代,而不需要達(dá)到默認(rèn)的分代年齡与倡。
- 大對(duì)象:由-XX:PretenureSizeThreshold啟動(dòng)參數(shù)控制界逛,若對(duì)象大小大于此值,就會(huì)繞過新生代, 直接在老年代中分配。
當(dāng)晉升到老年代的對(duì)象大于了老年代的剩余空間時(shí),就會(huì)觸發(fā)FGC(Major GC)捕犬,F(xiàn)GC處理的區(qū)域同時(shí)包括新生代和老年代枕荞。除此之外,還有以下4種情況也會(huì)觸發(fā)FGC: - 老年代的內(nèi)存使用率達(dá)到了一定閾值(可通過參數(shù)調(diào)整),直接觸發(fā)FGC。
- 空間分配擔(dān)保:在YGC之前,會(huì)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象的總空間赞别。如果小于,說明YGC是不安全的配乓,則會(huì)查看參數(shù) HandlePromotionFailure 是否被設(shè)置成了允許擔(dān)保失敗仿滔,如果不允許則直接觸發(fā)Full GC;如果允許犹芹,那么會(huì)進(jìn)一步檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小崎页,如果小于也會(huì)觸發(fā) Full GC。
- Metaspace(元空間)在空間不足時(shí)會(huì)進(jìn)行擴(kuò)容腰埂,當(dāng)擴(kuò)容到了-XX:MetaspaceSize 參數(shù)的指定值時(shí)飒焦,也會(huì)觸發(fā)FGC。
- System.gc() 或者Runtime.gc() 被顯式調(diào)用時(shí)屿笼,觸發(fā)FGC牺荠。
4. 在什么情況下翁巍,GC會(huì)對(duì)程序產(chǎn)生影響?
不管YGC還是FGC休雌,都會(huì)造成一定程度的程序卡頓(即Stop The World問題:GC線程開始工作灶壶,其他工作線程被掛起),即使采用ParNew杈曲、CMS或者G1這些更先進(jìn)的垃圾回收算法例朱,也只是在減少卡頓時(shí)間,而并不能完全消除卡頓鱼蝉。
那到底什么情況下,GC會(huì)對(duì)程序產(chǎn)生影響呢箫荡?根據(jù)嚴(yán)重程度從高到底魁亦,我認(rèn)為包括以下4種情況:
- FGC過于頻繁:FGC通常是比較慢的,少則幾百毫秒羔挡,多則幾秒洁奈,正常情況FGC每隔幾個(gè)小時(shí)甚至幾天才執(zhí)行一次,對(duì)系統(tǒng)的影響還能接受绞灼。但是利术,一旦出現(xiàn)FGC頻繁(比如幾十分鐘就會(huì)執(zhí)行一次),這種肯定是存在問題的低矮,它會(huì)導(dǎo)致工作線程頻繁被停止印叁,讓系統(tǒng)看起來一直有卡頓現(xiàn)象,也會(huì)使得程序的整體性能變差军掂。
- YGC耗時(shí)過長:一般來說轮蜕,YGC的總耗時(shí)在幾十或者上百毫秒是比較正常的,雖然會(huì)引起系統(tǒng)卡頓幾毫秒或者幾十毫秒蝗锥,這種情況幾乎對(duì)用戶無感知跃洛,對(duì)程序的影響可以忽略不計(jì)。但是如果YGC耗時(shí)達(dá)到了1秒甚至幾秒(都快趕上FGC的耗時(shí)了)终议,那卡頓時(shí)間就會(huì)增大汇竭,加上YGC本身比較頻繁,就會(huì)導(dǎo)致比較多的服務(wù)超時(shí)問題穴张。
- FGC耗時(shí)過長:FGC耗時(shí)增加细燎,卡頓時(shí)間也會(huì)隨之增加,尤其對(duì)于高并發(fā)服務(wù)陆馁,可能導(dǎo)致FGC期間比較多的超時(shí)問題找颓,可用性降低,這種也需要關(guān)注叮贩。
- YGC過于頻繁:即使YGC不會(huì)引起服務(wù)超時(shí)击狮,但是YGC過于頻繁也會(huì)降低服務(wù)的整體性能佛析,對(duì)于高并發(fā)服務(wù)也是需要關(guān)注的。
其中彪蓬,「FGC過于頻繁」和「YGC耗時(shí)過長」寸莫,這兩種情況屬于比較典型的GC問題,大概率會(huì)對(duì)程序的服務(wù)質(zhì)量產(chǎn)生影響档冬。剩余兩種情況的嚴(yán)重程度低一些膘茎,但是對(duì)于高并發(fā)或者高可用的程序也需要關(guān)注。
03 排查FGC問題的實(shí)踐指南
通過上面的案例分析以及理論介紹酷誓,再總結(jié)下FGC問題的排查思路披坏,作為一份實(shí)踐指南供大家參考。
- 清楚從程序角度盐数,有哪些原因?qū)е翭GC棒拂?
- 大對(duì)象:系統(tǒng)一次性加載了過多數(shù)據(jù)到內(nèi)存中(比如SQL查詢未做分頁),導(dǎo)致大對(duì)象進(jìn)入了老年代玫氢。
- 內(nèi)存泄漏:頻繁創(chuàng)建了大量對(duì)象帚屉,但是無法被回收(比如IO對(duì)象使用完后未調(diào)用close方法釋放資源),先引發(fā)FGC漾峡,最后導(dǎo)致OOM.
- 程序頻繁生成一些長生命周期的對(duì)象攻旦,當(dāng)這些對(duì)象的存活年齡超過分代年齡時(shí)便會(huì)進(jìn)入老年代,最后引發(fā)FGC. (即本文中的案例)
- 程序BUG導(dǎo)致動(dòng)態(tài)生成了很多新類生逸,使得 Metaspace 不斷被占用牢屋,先引發(fā)FGC,最后導(dǎo)致OOM.
- 代碼中顯式調(diào)用了gc方法牺陶,包括自己的代碼甚至框架中的代碼伟阔。
- JVM參數(shù)設(shè)置問題:包括總內(nèi)存大小、新生代和老年代的大小掰伸、Eden區(qū)和S區(qū)的大小皱炉、元空間大小、垃圾回收算法等等狮鸭。
- 清楚排查問題時(shí)能使用哪些工具
- 公司的監(jiān)控系統(tǒng):大部分公司都會(huì)有合搅,可全方位監(jiān)控JVM的各項(xiàng)指標(biāo)。
- JDK的自帶工具歧蕉,包括jmap灾部、jstat等常用命令:# 查看堆內(nèi)存各區(qū)域的使用率以及GC情況jstat -gcutil -h20 pid 1000# 查看堆內(nèi)存中的存活對(duì)象,并按空間排序jmap -histo pid | head -n20# dump堆內(nèi)存文件jmap -dump:format=b,file=heap pid
- 可視化的堆內(nèi)存分析工具:JVisualVM惯退、MAT等
- 排查指南
- 查看監(jiān)控赌髓,以了解出現(xiàn)問題的時(shí)間點(diǎn)以及當(dāng)前FGC的頻率(可對(duì)比正常情況看頻率是否正常)
- 了解該時(shí)間點(diǎn)之前有沒有程序上線、基礎(chǔ)組件升級(jí)等情況。
- 了解JVM的參數(shù)設(shè)置锁蠕,包括:堆空間各個(gè)區(qū)域的大小設(shè)置夷野,新生代和老年代分別采用了哪些垃圾收集器,然后分析JVM參數(shù)設(shè)置是否合理荣倾。
- 再對(duì)步驟1中列出的可能原因做排除法悯搔,其中元空間被打滿、內(nèi)存泄漏舌仍、代碼顯式調(diào)用gc方法比較容易排查妒貌。
- 針對(duì)大對(duì)象或者長生命周期對(duì)象導(dǎo)致的FGC,可通過 jmap -histo 命令并結(jié)合dump堆內(nèi)存文件作進(jìn)一步分析铸豁,需要先定位到可疑對(duì)象灌曙。
- 通過可疑對(duì)象定位到具體代碼再次分析,這時(shí)候要結(jié)合GC原理和JVM參數(shù)設(shè)置节芥,弄清楚可疑對(duì)象是否滿足了進(jìn)入到老年代的條件才能下結(jié)論平匈。
文章來源:https://baijiahao.baidu.com/s?id=1666710293511563448&wfr=spider&for=pc
文章內(nèi)容很好,所以直接復(fù)制過來藏古,感謝作者:駱俊武