1. 程式人生 > >解Bug之路-記一次JVM堆外記憶體洩露Bug的查詢

解Bug之路-記一次JVM堆外記憶體洩露Bug的查詢

# 解Bug之路-記一次JVM堆外記憶體洩露Bug的查詢 ## 前言 JVM的堆外記憶體洩露的定位一直是個比較棘手的問題。此次的Bug查詢從堆內記憶體的洩露反推出堆外記憶體,同時對實體記憶體的使用做了定量的分析,從而實錘了Bug的源頭。筆者將此Bug分析的過程寫成部落格,以饗讀者。 由於實體記憶體定量分析部分用到了linux kernel虛擬記憶體管理的知識,讀者如果有興趣瞭解請看ulk3(《深入理解linux核心第三版》) ## 記憶體洩露Bug現場 一個線上穩定運行了三年的系統,從物理機遷移到docker環境後,運行了一段時間,突然被監控系統發出了某些例項不可用的報警。所幸有負載均衡,可以自動下掉節點,如下圖所示: ![gc_local](https://static.oschina.net/uploads/img/201801/08104437_3KUb.png "gc_local") 登入到對應機器上後,發現由於記憶體佔用太大,觸發OOM,然後被linux系統本身給kill了。 ## 應急措施 緊急在出問題的例項上再次啟動應用,啟動後,記憶體佔用正常,一切Okay。 ## 奇怪現象 當前設定的最大堆記憶體是1792M,如下所示: ``` -Xmx1792m -Xms1792m -Xmn900m -XX:PermSi ze=256m -XX:MaxPermSize=256m -server -Xss512k ``` 檢視作業系統層面的監控,發現記憶體佔用情況如下圖所示: ![gc_upper](https://static.oschina.net/uploads/img/201801/08104501_x86B.png "gc_upper") 上圖藍色的線表示總的記憶體使用量,發現一直漲到了4G後,超出了系統限制。 很明顯,有堆外記憶體洩露了。 ## 查詢線索 ### gc日誌 一般出現記憶體洩露,筆者立馬想到的就是檢視當時的gc日誌。 本身應用所採用框架會定時打印出對應的gc日誌,遂檢視,發現gc日誌一切正常。對應日誌如下: ![gc_log](https://static.oschina.net/uploads/img/201801/08104524_biUf.png "gc_log") 查看了當天的所有gc日誌,發現記憶體始終會回落到170M左右,並無明顯的增加。要知道JVM程序本身佔用的記憶體可是接近4G(加上其它程序,例如日誌程序就已經到4G了),進一步確認是堆外記憶體導致。 ### 排查程式碼 開啟線上服務對應對應程式碼,查了一圈,發現沒有任何地方顯式利用堆外記憶體,其沒有依賴任何額外的native方法。關於網路IO的程式碼也是託管給Tomcat,很明顯,作為一個全世界廣泛流行的Web伺服器,Tomcat不大可能有堆外記憶體洩露。 ## 進一步查詢 由於在程式碼層面沒有發現堆外記憶體的痕跡,那就繼續找些其它的資訊,希望能發現蛛絲馬跡。 ### Dump出JVM的Heap堆 由於線上出問題的Server已經被kill,還好有其它幾臺,登上去發現它們也 佔用了很大的堆外記憶體,只是還沒有到觸發OOM的臨界點而已。於是就趕緊用jmap dump了兩臺機器中應用JVM的堆情況,這兩臺留做現場保留不動,然後將其它機器迅速重啟,以防同時被OOM導致服務不可用。 使用如下命令dump: ``` jmap -dump:format=b,file=heap.bin [pid] ``` ### 使用MAT分析Heap檔案 挑了一個heap檔案進行分析,堆的使用情況如下圖所示: ![gc_heap_dump](https://static.oschina.net/uploads/img/201801/08104550_ukiC.png "gc_heap_dump") 一共用了200多M,和之前gc檔案打印出來的170M相差不大,遠遠沒有到4G的程度。 不得不說MAT是個非常好用的工具,它可以提示你可能記憶體洩露的點: ![gc_cached_bns_client](https://static.oschina.net/uploads/img/201801/08104610_EzHN.png "gc_cached_bns_client") 這個cachedBnsClient類有12452個例項,佔用了整個堆的61.92%。 查看了另一個heap檔案,發現也是同樣的情況。這個地方肯定有記憶體洩露,但是也佔用了130多M,和4G相差甚遠。 ### 檢視對應的程式碼 系統中大部分對於CachedBnsClient的呼叫,都是通過註解Autowired的,這部分例項數很少。 唯一頻繁產生此類例項的程式碼如下所示: ``` @Override public void fun() { BnsClient bnsClient = new CachedBnsClient(); // do something return ; } ``` 此CachedBnsClient僅僅在方法體內使用,並沒有逃逸到外面,再看此類本身 ``` public class CachedBnsClient { private Concurren