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