解Bug之路-記一次JVM堆外記憶體洩露Bug的查詢
阿新 • • 發佈:2020-06-09
# 解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