本文首發於公衆號【 程序員大帝 】,關注第一時間獲取git
一塊兒碼動人生,成爲Coding King!!!程序員
GitHub上已經開源 github.com/BeKingCodin… , 一線大廠面試核心知識點、個人聯繫方式和技術交流羣,歡迎Star和完善github
一般來講,一個系統在上線以前應該通過多輪的調試,在測試服務器上穩定的運行過一段時間。咱們知道 Full GC 會致使 Stop The World 狀況的出現,嚴重影響性能,因此一個性能良好的 JVM,應該幾天纔會發生一次 Full GC,或者最多一天幾回而已。面試
可是昨天晚上忽然收到短信通知,顯示線上部署的四臺機器所有卡死,服務所有不可用,趕忙查看問題!數據庫
涉及到相似的錯誤,最開始三板斧確定是查看 JVM 的狀況。不少中小型公司沒有創建可視化的監控平臺,好比Zabbix、Ganglia、Open-Falcon、Prometheus等等,沒辦法直接可視化看到JVM各個區域的內存變化,GC次數和GC耗時。服務器
不過不用怕,我們用 jstat 這種工具也能夠。言歸正傳,排查了線上問題以後,發現居然是服務器上面,JVM 這段時間瘋狂 Full GC,一天搞了幾十次,直接把系統卡死了!數據結構
破案以前,咱們先來要保護下案發現場並進行排查。工具
機器配置 | 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。此時整個系統的內存模型圖以下所示:
根據對案發現場的排查,咱們能夠還原線上系統的 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,因此咱們還要有更多的證據去驗證咱們的判斷。
老年代裏到底爲何會有那麼多的對象?
面對這個問題,說句實話,僅僅根據可視化監控和推論是絕對無法往下分析了,由於咱們並不知逋老年代裏到底爲何會有那麼多的對象。這個時候就有必要讓線上系統從新運行,藉助 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 !
分析到這裏,後面的過程就很簡單了,咱們能夠經過 jmap 工具,dump 出內存快照,而後再用 jhat 或者 Visual VM 之類的可視化工具來分析就能夠了。
經過內存快照的分析,直接定位出來那個幾百MB的大對象,發現居然是個Map之類的數據結構,這是什麼鬼?
返回頭去開始擼代碼,發現是從數據庫裏查出來的數據存放在了這個Map裏,沒有好辦法,再繼續地毯式排查。
最後發現居然是有條坑爹的 SQL 語句沒加 where條件!!不知道是手滑仍是忘了,測試的時候這個分支也沒走到(畫外音:這段代碼的開發和測試都不是我)
沒有 where 條件,每次查詢確定會有超出預期的大量數據,致使了每隔一段時間就會搞出幾個上百 MB 的大對象,這些對象所有直接進入老年代,而後觸發 Full GC !
破案定位嫌疑人最困難,在知道兇手後,靠着滿大街的攝像頭,抓人就是分分鐘的事情。因此咱們排查到這裏,這個案例如何解決已經很是簡單了。
(1)解決代碼中的 bug,把錯誤的 SQL 修復,一勞永逸完全解決這幾百 MB 大對象進入老年代的問題。
(2)以防萬一,對堆內存空間進行擴容,而後再把-XX:CMSInitiatingOccupancyFraction 參數從 62 調高,使得老年代的使用率更高時纔會觸發 Full GC。
這兩個步驟優化完畢以後,線上系統基本上表現就很是好了。
本文經過一個線上系統卡死的現象,詳細地定位並剖析了產生問題的緣由。也證實了要成爲一個優秀的程序員,不光對語言自己要有所瞭解,還要對 JVM 調優這樣偏底層的知識有所涉獵,這對排查問題會有很是大的幫助。同時完善的監控很是重要,經過提早告警,能夠將問題扼殺在搖籃裏!
好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是人才。
我後面會持續更新《Offer收割機》系列和互聯網經常使用技術棧相關的文章,很是感謝各位老闆能看到這裏,若是這個文章寫得還不錯,以爲我有點東西的話,求點贊👍 求關注❤️ 求分享👥 對我來講真的很是有用!!!
創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!
Craig無忌 | 文 【原創】【轉載請聯繫本人】 若是本篇博客有任何錯誤,請批評指教,不勝感激 !
《Offer收割機》系列會持續更新,能夠關注個人公衆號「 程序員大帝 」第一時間閱讀和催更(公衆號比博客早一到兩篇喲),本文GitHub上已經收錄 github.com/BeKingCodin… ,會對一線大廠面試要求的核心知識點持續講解、並有對標阿里P7級別的成長體系腦圖,歡迎加入技術交流羣,咱們一塊兒有點東西。