重大事故!線上系統頻繁卡死,兇手居然是 Full GC ?

本文首發於公衆號【 程序員大帝 】,關注第一時間獲取git

一塊兒碼動人生,成爲Coding King!!!程序員

GitHub上已經開源 github.com/BeKingCodin… , 一線大廠面試核心知識點、個人聯繫方式和技術交流羣,歡迎Star和完善github

在這裏插入圖片描述

01 案發現場

一般來講,一個系統在上線以前應該通過多輪的調試,在測試服務器上穩定的運行過一段時間。咱們知道 Full GC 會致使 Stop The World 狀況的出現,嚴重影響性能,因此一個性能良好的 JVM,應該幾天纔會發生一次 Full GC,或者最多一天幾回而已。面試

可是昨天晚上忽然收到短信通知,顯示線上部署的四臺機器所有卡死,服務所有不可用,趕忙查看問題!數據庫

涉及到相似的錯誤,最開始三板斧確定是查看 JVM 的狀況。不少中小型公司沒有創建可視化的監控平臺,好比Zabbix、Ganglia、Open-Falcon、Prometheus等等,沒辦法直接可視化看到JVM各個區域的內存變化,GC次數和GC耗時。服務器

不過不用怕,我們用 jstat 這種工具也能夠。言歸正傳,排查了線上問題以後,發現居然是服務器上面,JVM 這段時間瘋狂 Full GC,一天搞了幾十次,直接把系統卡死了!數據結構

02 排查現場

破案以前,咱們先來要保護下案發現場並進行排查。工具

在這裏插入圖片描述

機器配置 2核 4G
JVM堆內存大小 2G
系統運行時間 1天
Full GC出現次數和單次耗時 48次,300ms左右
Young GC出現次數和單次耗時 4000屢次,50ms

這樣看起來,系統的性能是至關差了,一分鐘三次 Young GC,每半小時就一次 Full GC,接下來咱們再看看 JVM 的參數。可能有的同窗每次見到這麼多參數都會頭大,但其實每個參數的背後都會透漏着蛛絲馬跡。我這裏摘取關鍵參數以下:性能

CMSInitiatingOccupancyFraction 62
SurvivorRatio 5
Xmx 1536M
Xmx 1536M

簡單解讀一下,根據參數能夠看出來,這臺 4G 的機器上爲 JVM 的堆內存是設置了 1.5G 左右的大小。新生代和老年代默認會按照 1:2 的比例進行劃分,分別對應 512M 和 1G。測試

一個重要的參數 XXiSurvivorRatio 設置爲5,它所表明的是新生代中Eden:Survivor1 :Survivor2的比例是 5:1:1。因此此時Eden區域大體爲365M,每一個Survivor區域大體爲70MB。

還有一個很是關鍵的參數,那就是 CMSInitiatingOccupancyFraction 設置爲了62。它意味着一旦老年代內存佔用達到 62%,也就是存在 620MB 左右對象時,就會觸發一次 Full GC。此時整個系統的內存模型圖以下所示:

在這裏插入圖片描述

03 還原現場

根據對案發現場的排查,咱們能夠還原線上系統的 GC 運行狀況了,分析一下線上的 JVM 到底出現了什麼情況。

首先咱們知道每分鐘會發生 3 次 Young GC,說明系統運行 20 秒後就會把 Eden 區塞滿,Eden 區一共有 356MB 的空間,所以平均下來系統每秒鐘會產生 20MB 左右大小的對象。

接着咱們根據每半小時觸發一次 Full GC 的推斷,以及 「-XX:CMSmitiatingOccupancyFraction=62」 參數的設置,老年代有 1G 的空間,因此應該是在老年代有 600多MB 左右的對象時就會觸發一次 Full GC。

看到這裏,有的同窗可能馬上下結論,以爲確定是由於 Survivor 區域過小了,致使 Young GC 後的存活對象太多放不下,就一直有對象流入老年代,進而致使後來觸發的 Full GC ?

實際上分析到這裏,絕對不能草率下這個判斷。

由於老年代裏爲何有那麼多的對象?確實有多是由於每次 Young GC後的存活對象較多,Survivor區域過小,放不下了。

但也有多是長時間存活的對象太多了,都積累在老年代裏,始終回收不掉,進而致使老年代很容易就達到 62% 的佔比觸發 Full GC,因此咱們還要有更多的證據去驗證咱們的判斷。

04 破案開始

老年代裏到底爲何會有那麼多的對象?

面對這個問題,說句實話,僅僅根據可視化監控和推論是絕對無法往下分析了,由於咱們並不知逋老年代裏到底爲何會有那麼多的對象。這個時候就有必要讓線上系統從新運行,藉助 jstat 工具實時去觀察 JVM 實際的運行狀況。這個過程很是相似警察叔叔在破案時,會假設本身是兇手,嘗試再現當時的場景。

這裏省略具體的 jstat 工具操做過程,若是你們沒有接觸過百度下便可,很是簡單。經過 jstat 的觀察,咱們當時能夠看到,每次 Young GC 事後升入老年代裏的對象其實不多。

看到這個現象,我起初也很奇怪。由於經過 jstat 的追蹤觀察,並非每次Young GC 後都有 幾十MB 對象進入老年代的,而是偶爾一次 Young GC 纔會有 幾十MB 對象進入老年代!

因此正常來講,既然沒有對象重新生代過渡到老年代,那麼老年代就不至於快速的到達 62% 的佔有率,從而致使 Full GC。那麼老年代觸發 Full GC 時候的幾百 MB 對象到底從哪裏來的?

在這裏插入圖片描述

仔細一想,其實答案已經呼之欲出了,那就是大對象!

必定是系統運行的時候,每隔一段時間就會忽然產生幾百 MB 的大對象,因爲新生代放不下,因此會直接進入老年代,而不會走 Eden 區域。而後再配合上年輕代還偶爾會有 Young GC 後幾十 MB 對象進入老年代,因此不停地觸發Full GC !

05 抓捕真兇

分析到這裏,後面的過程就很簡單了,咱們能夠經過 jmap 工具,dump 出內存快照,而後再用 jhat 或者 Visual VM 之類的可視化工具來分析就能夠了。

經過內存快照的分析,直接定位出來那個幾百MB的大對象,發現居然是個Map之類的數據結構,這是什麼鬼?

返回頭去開始擼代碼,發現是從數據庫裏查出來的數據存放在了這個Map裏,沒有好辦法,再繼續地毯式排查。

最後發現居然是有條坑爹的 SQL 語句沒加 where條件!!不知道是手滑仍是忘了,測試的時候這個分支也沒走到(畫外音:這段代碼的開發和測試都不是我)

沒有 where 條件,每次查詢確定會有超出預期的大量數據,致使了每隔一段時間就會搞出幾個上百 MB 的大對象,這些對象所有直接進入老年代,而後觸發 Full GC !

06 善後處理

破案定位嫌疑人最困難,在知道兇手後,靠着滿大街的攝像頭,抓人就是分分鐘的事情。因此咱們排查到這裏,這個案例如何解決已經很是簡單了。

(1)解決代碼中的 bug,把錯誤的 SQL 修復,一勞永逸完全解決這幾百 MB 大對象進入老年代的問題。

(2)以防萬一,對堆內存空間進行擴容,而後再把-XX:CMSInitiatingOccupancyFraction 參數從 62 調高,使得老年代的使用率更高時纔會觸發 Full GC。

這兩個步驟優化完畢以後,線上系統基本上表現就很是好了。

07 總結

本文經過一個線上系統卡死的現象,詳細地定位並剖析了產生問題的緣由。也證實了要成爲一個優秀的程序員,不光對語言自己要有所瞭解,還要對 JVM 調優這樣偏底層的知識有所涉獵,這對排查問題會有很是大的幫助。同時完善的監控很是重要,經過提早告警,能夠將問題扼殺在搖籃裏!

平常求贊

好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是人才。

我後面會持續更新《Offer收割機》系列和互聯網經常使用技術棧相關的文章,很是感謝各位老闆能看到這裏,若是這個文章寫得還不錯,以爲我有點東西的話,求點贊👍 求關注❤️ 求分享👥 對我來講真的很是有用!!!

創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!

Craig無忌 | 文 【原創】【轉載請聯繫本人】 若是本篇博客有任何錯誤,請批評指教,不勝感激 !


《Offer收割機》系列會持續更新,能夠關注個人公衆號「 程序員大帝 」第一時間閱讀和催更(公衆號比博客早一到兩篇喲),本文GitHub上已經收錄 github.com/BeKingCodin… ,會對一線大廠面試要求的核心知識點持續講解、並有對標阿里P7級別的成長體系腦圖,歡迎加入技術交流羣,咱們一塊兒有點東西。

在這裏插入圖片描述
相關文章
相關標籤/搜索