當前位置:
首頁 > 最新 > 一次線上內存泄露歷險

一次線上內存泄露歷險

故事

剛進公司那段時間,在敏捷項目制的執行下,需求有條不紊地進行著。某個周末,業務系統反饋群內,操作人員反饋系統不可用,我們急忙尋求運維的幫助,將系統重啟並恢復使用。同時排查相關log,檢查異常點,但是根據log並沒有跟蹤出結果。於是想到是否有OOM的dump文件生成,詢問運維後,被告知並沒有生成。諮詢之前的應用負責人,以前也有類似系統不可用情況,但只是偶現。沒有辦法,根據應用日誌查不出結果,只有下次復現時導出dump徹查了。又過去一段時間,故障反饋群里又是一樣的問題,於是趕忙麻煩運維把dump生成,然後重啟了應用,同時離線對dump進行了分析。

通過分析,在內存泄漏的可疑點內,PoolingHttpClientConnectionManager這個類映入眼帘,jvm居然包含了近15萬個該類的實例,所佔內存大小是1,918,318,216 bytes,換算成Mb就是1829M,如此多的實例沒有被回收,勢必會造成OOM,導致系統不可用。

於是查找源碼,發現是操作阿里雲oss的相關代碼,IdleConnectionReaper類的變數有一個ArrayList,是由static修飾的,由static修飾想必大家都知道結果了: 這類強引用,虛擬機GC是無法進行回收的。

然而問題來了,為何ArrayList里的HttpClientConnectionManager對象一直在增加?

帶著疑問我們查看了代碼中OSS的調用入口,在調用入口處,發現一處可疑代碼:

再看摘錄的相關調用,發現了HttpClientConnectionManager對象一直在增加的根源

但是翻閱IdleConnectionReaper類的源碼時,發現了removeConnectionManager方法,用來移除ArrayList內的HttpClientConnectionManager對象。

同時我們對比了阿里雲oss官方給的demo和涉及類的類圖, demo中是調用了shutdown方法,shutdown的實現就是調用IdleConnectionReaper的removeConnectionManager方法。

回過頭來,在我們的代碼里並沒有搜索到shutdown相關的調用。也就是說,每次調用oss,都會往IdleConnectionReaper的connectionManagers變數里增加對象,而且整個生命周期內都沒有對其進行對象移除,最終導致內存溢出。

解決方案要麼在方法調用的最後進行shutdown操作;要麼就避免對象一直創建,用連接池進行管理,提供性能和效率。

於是我們聯繫了基礎服務組,報告了該問題。基礎服務組給出了補丁,我們也配合進行了驗證,並上線進行了修復和觀察,這段內存泄漏的經歷便告一段落。

疑問

有一個問題一直困擾著我們,隨著時間的推移,有問題的那個靜態變數ArrayList遲早會把內存撐爆掉,理論上該問題應該在線上一直存在,為何一直沒暴露(或者說偶爾暴露)。我們帶著疑問對這次經歷進行總結和復盤:

知識點總結

說完故事,就要來總結枯燥的知識點了。大家都知道這次問題的罪魁禍首是內存泄漏。而什麼是內存泄漏,導致內存泄漏的原因是什麼,出現疑似內存泄漏後又該如何定位呢?

1. 內存泄漏的定義

內存泄漏是指無用對象(不再使用的對象)持續佔有內存或無用對象的內存得不到及時釋放,從而造成內存空間的浪費稱為內存泄漏。

2. 引起內存泄漏的幾種情況

2.1 靜態集合類引起內存泄漏

故事中static修飾的ArrayList是一個絕佳的例子,這些靜態變數的生命周期和應用程序一致,他們所引用的所有的對象Object也不能被釋放,因為他們一直被引用著。

2.2當集合裡面的對象屬性被修改後,再調用remove()方法時不起作用

例如HashMap、HashSet,當集合內的對象屬性參與了hash的計算,改變對象屬性後,再去調用remove()方法,無法將集合內的對象移除。

2.3監聽器

在釋放對象的時候卻沒有去刪除這些監聽器,增加了內存泄漏的機會。

2.4各種連接

比如資料庫連接(dataSourse.getConnection()),網路連接(socket)和io連接,除非其顯式的調用了其close()方法將其連接關閉,否則是不會自動被GC 回收的。

2.5單例模式

不正確使用單例模式是引起內存泄漏的一個常見問題,單例對象在初始化後將在JVM的整個生命周期中存在(以靜態變數的方式),如果單例對象持有外部的引用,那麼這個對象將不能被JVM正常回收,導致內存泄漏。

3. 定位內存泄漏的相關工具

在本文故事裡我們用到了下面這些工具來輔助我們定位內存泄漏:

3.1 Java自帶的強大工具

jstat:虛擬機統計信息監控工具--可實時查看目前虛擬機相關統計信息。

使用場景:利用jstat可以快速查看當前時刻jvm的gc情況,是否有full gc過於頻繁一目了然。

jmap:虛擬機內存映像工具—可生成即時虛擬機內存dump,供離線分析。

使用場景:在jvm啟動參數里我們可以通過-XX:+HeapDumpOnOutOfMemoryError和-XX:+HeapDumpPath來設置發生OOM時導出堆到文件,或者我們可以通過jmap來手動生成堆轉儲文件。

jvisualvm可視化工具,可實時分析內存佔用、gc、線程等。

使用場景:需要實時分析虛擬機內存時使用,可直觀看到堆使用情況

jconsole:和jvisualvm大同小異,都是可視化的工具。

3.2 Eclipse的M(emory)A(nalizer)T(ool)

Eclipse MAT是一個快速且功能豐富的Java Heap分析工具, 可以幫助我們尋找內存泄露, 減少內存消耗。MAT可以分析程序生成的Heap dumps文件, 它會快速計算出對象的Retained Size, 來展示是哪些對象沒有被GC, 自動生成內存泄露疑點的報告。

使用場景:應用dump文件生成後,導入至MAT中,可快速生成內存泄漏的報告,以供分析。

這些工具的具體使用方法都可以在搜索引擎里檢索到,這裡就不深入展開了。

3.3 開源的實時監控系統

開源的實時監控系統有很多,這裡我們談到的是CAT。CAT是大眾點評開源的監控項目,有一項heartbeat面板可以實時查看應用的gc情況,內存使用情況,這對下面的偶現問題排查起到了關鍵作用。

偶現問題原因排查

結合實際我們對線上偶現的問題進行剖析,並初步提出幾點推測:

1. 最近oss封裝版本有更新,引入了未調用shutdown方法的bug

通過抽樣查閱oss幾個歷史的封裝jar包,均未包含shutdown方法,說明問題一直存在,排除該原因。

2. 是否有人手動重啟應用,短時間內避免了內存溢出

聯繫運維同學確認後,並未有人手動重啟應用,排除此可能。

3. 應用敏捷迭代更新

可能有同學會有疑問,敏捷迭代怎麼會導致問題偶現。我們目前的敏捷迭代周期是兩周,如果應用的old區容量能夠撐兩周,那麼迭代上線,就會讓應用重啟,從而使得jvm的世界從頭開始,內存溢出就不會暴露;而隨著系統的頻繁使用和需求的飽和,應用不能保證每次都能在old區滿之前來個上線讓應用重啟,於是內存溢出就出現了,當然這只是個推測,需要證據來支撐。

偶然的一次監控告警,發現了CAT上有個Heartbeat面板,展示各個應用的gc情況和堆內存使用情況,於是查看了歷史old區使用情況,果然有一個時間點出現old區使用容量驟降,再匹配時間點,恰好是有應用上線。CAT的證據支持了我們的這一推斷,解決了偶現問題的困擾。

問題過程回顧與建議

1. 系統使用人員報告故障

2. 聯繫運維查看gc回收情況(使用jstat命令)或查看CAT的heartbeat面板

3. 聯繫運維生成當時的heap dump文件(使用jmap命令)

4. 應用重啟恢復使用(臨時解決系統不可用)

5. 離線分析原因

5.1 離線分析dump

5.2 結合監控平台CAT查看應用不可用前後jvm內存情況和gc情況

5.3 分析出內存泄漏點

6. 本地開發環境嘗試問題復現

7. 找出問題源並聯繫相關人員修復

8. 更新修復補丁並驗證問題是否解決

當碰到疑似內存泄漏問題,可以參考以上過程回顧,如果設置了HeapDumpOnOutOfMemoryError卻沒有生成堆轉儲文件的,一定要聯繫運維手動生成堆轉儲再進行重啟,否則就錯失了分析dump的絕佳時機;至於在coding時如何避免內存泄漏,只需針對造成內存泄漏的幾點原因稍加規避即可。

參考文獻:《深入了解Java虛擬機》

部分源碼來自阿里雲

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 米么騷客 的精彩文章:

TAG:米么騷客 |