重大事故!線上系統(tǒng)頻繁卡死,兇手竟然是 Full GC ?
掃描二維碼
隨時(shí)隨地手機(jī)看文章
每日雞湯,好喝
01 案發(fā)現(xiàn)場
通常來說,一個(gè)系統(tǒng)在上線之前應(yīng)該經(jīng)過多輪的調(diào)試,在測試服務(wù)器上穩(wěn)定的運(yùn)行過一段時(shí)間。我們知道 Full GC 會(huì)導(dǎo)致 Stop The World 情況的出現(xiàn),嚴(yán)重影響性能,所以一個(gè)性能良好的 JVM,應(yīng)該幾天才會(huì)發(fā)生一次 Full GC,或者最多一天幾次而已。
但是昨天晚上突然收到短信通知,顯示線上部署的四臺機(jī)器全部卡死,服務(wù)全部不可用,趕緊查看問題!
涉及到類似的錯(cuò)誤,最開始三板斧肯定是查看 JVM 的情況。很多中小型公司沒有建立可視化的監(jiān)控平臺,比如Zabbix、Ganglia、Open-Falcon、Prometheus等等,沒辦法直接可視化看到JVM各個(gè)區(qū)域的內(nèi)存變化,GC次數(shù)和GC耗時(shí)。
不過不用怕,咱們用 jstat 這種工具也可以。言歸正傳,排查了線上問題之后,發(fā)現(xiàn)竟然是服務(wù)器上面,JVM 這段時(shí)間瘋狂 Full GC,一天搞了幾十次,直接把系統(tǒng)卡死了!
02 排查現(xiàn)場
破案之前,我們先來要保護(hù)下案發(fā)現(xiàn)場并進(jìn)行排查。
機(jī)器配置 | 2核 4G |
JVM堆內(nèi)存大小 | 2G |
系統(tǒng)運(yùn)行時(shí)間 |
1天 |
Full GC出現(xiàn)次數(shù)和單次耗時(shí) | 48次,300ms左右 |
Young GC出現(xiàn)次數(shù)和單次耗時(shí) | 4000多次,50ms |
這樣看起來,系統(tǒng)的性能是相當(dāng)差了,一分鐘三次 Young GC,每半小時(shí)就一次 Full GC,接下來我們再看看 JVM 的參數(shù)??赡苡械耐瑢W(xué)每次見到這么多參數(shù)都會(huì)頭大,但其實(shí)每一個(gè)參數(shù)的背后都會(huì)透漏著蛛絲馬跡。我這里摘取關(guān)鍵參數(shù)如下:
CMSInitiatingOccupancyFraction | 62 |
SurvivorRatio | 5 |
Xmx |
1536M |
Xmx | 1536M |
簡單解讀一下,根據(jù)參數(shù)可以看出來,這臺 4G 的機(jī)器上為 JVM 的堆內(nèi)存是設(shè)置了 1.5G 左右的大小。新生代和老年代默認(rèn)會(huì)按照 1:2 的比例進(jìn)行劃分,分別對應(yīng) 512M 和 1G。
一個(gè)重要的參數(shù) XXiSurvivorRatio 設(shè)置為5,它所代表的是新生代中Eden:Survivor1 :Survivor2的比例是 5:1:1。所以此時(shí)Eden區(qū)域大致為365M,每個(gè)Survivor區(qū)域大致為70MB。
還有一個(gè)非常關(guān)鍵的參數(shù),那就是 CMSInitiatingOccupancyFraction 設(shè)置為了62。它意味著一旦老年代內(nèi)存占用達(dá)到 62%,也就是存在 620MB 左右對象時(shí),就會(huì)觸發(fā)一次 Full GC。此時(shí)整個(gè)系統(tǒng)的內(nèi)存模型圖如下所示:
03 還原現(xiàn)場
根據(jù)對案發(fā)現(xiàn)場的排查,我們可以還原線上系統(tǒng)的 GC 運(yùn)行情況了,分析一下線上的 JVM 到底出現(xiàn)了什么狀況。
首先我們知道每分鐘會(huì)發(fā)生 3 次 Young GC,說明系統(tǒng)運(yùn)行 20 秒后就會(huì)把 Eden 區(qū)塞滿,Eden 區(qū)一共有 356MB 的空間,因此平均下來系統(tǒng)每秒鐘會(huì)產(chǎn)生 20MB 左右大小的對象。
接著我們根據(jù)每半小時(shí)觸發(fā)一次 Full GC 的推斷,以及 “-XX:CMSmitiatingOccupancyFraction=62” 參數(shù)的設(shè)置,老年代有 1G 的空間,所以應(yīng)該是在老年代有 600多MB 左右的對象時(shí)就會(huì)觸發(fā)一次 Full GC。
看到這里,有的同學(xué)可能立刻下結(jié)論,覺得肯定是因?yàn)?Survivor 區(qū)域太小了,導(dǎo)致 Young GC 后的存活對象太多放不下,就一直有對象流入老年代,進(jìn)而導(dǎo)致后來觸發(fā)的 Full GC ?
實(shí)際上分析到這里,絕對不能草率下這個(gè)判斷。
因?yàn)槔夏甏餅槭裁从心敲炊嗟膶ο??確實(shí)有可能是因?yàn)槊看?Young GC后的存活對象較多,Survivor區(qū)域太小,放不下了。
但也有可能是長時(shí)間存活的對象太多了,都積累在老年代里,始終回收不掉,進(jìn)而導(dǎo)致老年代很容易就達(dá)到 62% 的占比觸發(fā) Full GC,所以我們還要有更多的證據(jù)去驗(yàn)證我們的判斷。
04 破案開始
老年代里到底為什么會(huì)有那么多的對象?
面對這個(gè)問題,說句實(shí)話,僅僅根據(jù)可視化監(jiān)控和推論是絕對沒法往下分析了,因?yàn)槲覀儾⒉恢屠夏甏锏降诪槭裁磿?huì)有那么多的對象。這個(gè)時(shí)候就有必要讓線上系統(tǒng)重新運(yùn)行,借助 jstat 工具實(shí)時(shí)去觀察 JVM 實(shí)際的運(yùn)行情況。這個(gè)過程非常類似警察叔叔在破案時(shí),會(huì)假設(shè)自己是兇手,嘗試再現(xiàn)當(dāng)時(shí)的場景。
這里省略具體的 jstat 工具操作過程,如果大家沒有接觸過百度下即可,非常簡單。通過 jstat 的觀察,我們當(dāng)時(shí)可以看到,每次 Young GC 過后升入老年代里的對象其實(shí)很少。
看到這個(gè)現(xiàn)象,我起初也很奇怪。因?yàn)橥ㄟ^ jstat 的追蹤觀察,并不是每次Young GC 后都有 幾十MB 對象進(jìn)入老年代的,而是偶爾一次 Young GC 才會(huì)有 幾十MB 對象進(jìn)入老年代!
所以正常來說,既然沒有對象從新生代過渡到老年代,那么老年代就不至于快速的到達(dá) 62% 的占有率,從而導(dǎo)致 Full GC。那么老年代觸發(fā) Full GC 時(shí)候的幾百 MB 對象到底從哪里來的?
仔細(xì)一想,其實(shí)答案已經(jīng)呼之欲出了,那就是大對象!
一定是系統(tǒng)運(yùn)行的時(shí)候,每隔一段時(shí)間就會(huì)突然產(chǎn)生幾百 MB 的大對象,由于新生代放不下,所以會(huì)直接進(jìn)入老年代,而不會(huì)走 Eden 區(qū)域。然后再配合上年輕代還偶爾會(huì)有 Young GC 后幾十 MB 對象進(jìn)入老年代,所以不停地觸發(fā)Full GC !
05 抓捕真兇
分析到這里,后面的過程就很簡單了,我們可以通過 jmap 工具,dump 出內(nèi)存快照,然后再用 jhat 或者 Visual VM 之類的可視化工具來分析就可以了。
通過內(nèi)存快照的分析,直接定位出來那個(gè)幾百M(fèi)B的大對象,發(fā)現(xiàn)竟然是個(gè)Map之類的數(shù)據(jù)結(jié)構(gòu),這是什么鬼?
返回頭去開始擼代碼,發(fā)現(xiàn)是從數(shù)據(jù)庫里查出來的數(shù)據(jù)存放在了這個(gè)Map里,沒有好辦法,再繼續(xù)地毯式排查。
最后發(fā)現(xiàn)竟然是有條坑爹的 SQL 語句沒加 where條件??!不知道是手滑還是忘了,測試的時(shí)候這個(gè)分支也沒走到(畫外音:這段代碼的開發(fā)和測試都不是我)
沒有 where 條件,每次查詢肯定會(huì)有超出預(yù)期的大量數(shù)據(jù),導(dǎo)致了每隔一段時(shí)間就會(huì)搞出幾個(gè)上百 MB 的大對象,這些對象全部直接進(jìn)入老年代,然后觸發(fā) Full GC !
06 善后處理
破案定位嫌疑人最困難,在知道兇手后,靠著滿大街的攝像頭,抓人就是分分鐘的事情。所以我們排查到這里,這個(gè)案例如何解決已經(jīng)非常簡單了。
(1)解決代碼中的 bug,把錯(cuò)誤的 SQL 修復(fù),一勞永逸徹底解決這幾百 MB 大對象進(jìn)入老年代的問題。
(2)以防萬一,對堆內(nèi)存空間進(jìn)行擴(kuò)容,然后再把-XX:CMSInitiatingOccupancyFraction 參數(shù)從 62 調(diào)高,使得老年代的使用率更高時(shí)才會(huì)觸發(fā) Full GC。
這兩個(gè)步驟優(yōu)化完畢之后,線上系統(tǒng)基本上表現(xiàn)就非常好了。
07 總結(jié)
本文通過一個(gè)線上系統(tǒng)卡死的現(xiàn)象,詳細(xì)地定位并剖析了產(chǎn)生問題的原因。也證明了要成為一個(gè)優(yōu)秀的程序員,不光對語言本身要有所了解,還要對 JVM 調(diào)優(yōu)這樣偏底層的知識有所涉獵,這對排查問題會(huì)有非常大的幫助。同時(shí)完善的監(jiān)控非常重要,通過提前告警,可以將問題扼殺在搖籃里!
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!