背景
我司商城系統生產服務隔一段時間就掛掉一次,所有的機器都有這個問題,而且問題出現的越來越頻繁,從最開始的半個月一次,到後來一週一次、3天一次,一直到最後的1天1次甚至2次,導致服務極其不穩定,查找泄漏源成了迫切要解決的問題
初步排查和猜測
1、首先獲取應用pid
ps -ef|grep marketing-center
2、根據pid查詢java應用堆內存使用情況,以及應用進程佔用系統內存情況
#查看java程序GC情況以及堆內內存使用情況
jstat -gc $pid 2000
#結果如下
[www@idc06-c-marketingcenter-03 ~]$ jstat -gc 14626 2000
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
0.0 65536.0 0.0 65536.0 2068480.0 737280.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 925696.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1118208.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1372160.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1554432.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1667072.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1699840.0 3436544.0 258445.0 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1839104.0 3436544.0 259483.3 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 65536.0 0.0 65536.0 2068480.0 1880064.0 3436544.0 259483.3 98432.0 92081.3 11392.0 10229.1 191 11.714 0 0.000 11.714
0.0 51200.0 0.0 51200.0 2082816.0 274432.0 3436544.0 245594.6 98432.0 92081.3 11392.0 10229.1 192 11.786 0 0.000 11.786
0.0 51200.0 0.0 51200.0 2082816.0 321536.0 3436544.0 245594.6 98432.0 92081.3 11392.0 10229.1 192 11.786 0 0.000 11.786
#查看應用進程佔用系統內存情況,可以用top代替
ps -p $pid -o rss,vsz
#結果如下:
[www@idc06-c-marketingcenter-03 ~]$ ps -p 14626 -o rss,vsz
RSS VSZ
12303720 19423396
根據以上信息可以看出,堆內內存使用也就5G左右,但是Java應用實際佔用內存卻高達12G左右,而且堆內內存一切正常,Young GC也比較正常平穩,Full GC也保持在一個較低的頻率,通過以上數據基本可以斷定發生了Java堆外內存泄漏,爲了驗證我的猜想,使用pmap命令查看一下系統內存分配
#查看系統內存分配情況
pmap -x $pid | sort -k3 -n
#結果如下,內容較多,截取關鍵部分展示
[www@idc06-c-marketingcenter-03 ~]$ pmap -x 14626 | sort -k3 -n
...
00007f44d8000000 65536 64712 64712 rw--- [ anon ]
00007f4578000000 65508 64744 64744 rw--- [ anon ]
00007f44d4000000 65524 64860 64860 rw--- [ anon ]
00007f43fc000000 65536 64996 64996 rw--- [ anon ]
00007f4464000000 65508 65004 65004 rw--- [ anon ]
00007f4528000000 65536 65044 65044 rw--- [ anon ]
00007f43dc000000 65536 65060 65060 rw--- [ anon ]
00007f45a4000000 65524 65148 65148 rw--- [ anon ]
00007f43d0000000 65508 65156 65156 rw--- [ anon ]
00007f45a0000000 65516 65180 65180 rw--- [ anon ]
...
發現大量64MB左右的內存塊,且分配地址在堆外,以上內容驗證了我的猜想,確實有堆外內存泄漏
詳細排查經過
由於是堆外內存泄漏,JDK自帶的工具已經不好用了,首先借助谷歌的內存分配監測工具gperftools
來排查具體哪段代碼進行了堆外內存申請,具體安裝使用請參考:Java直接內存泄漏排查工具gperftools使用方法
#結果較多,截取部分
Total: 125704.3 MB
94257.6 75.0% 75.0% 94257.6 75.0% updatewindow
20573.0 16.4% 91.3% 20573.0 16.4% inflateInit2_
9946.9 7.9% 99.3% 9946.9 7.9% os::malloc@91de80
457.8 0.4% 99.6% 457.8 0.4% init
253.1 0.2% 99.8% 20826.1 16.6% Java_java_util_zip_Inflater_init
149.0 0.1% 99.9% 149.0 0.1% readCEN
38.4 0.0% 100.0% 38.4 0.0% __GI__dl_allocate_tls
14.7 0.0% 100.0% 14.7 0.0% deflateInit2_
3.8 0.0% 100.0% 156.2 0.1% ZIP_Put_In_Cache0
2.4 0.0% 100.0% 2.4 0.0% _dl_new_object
2.2 0.0% 100.0% 2.2 0.0% newEntry
1.3 0.0% 100.0% 1.3 0.0% __GI___strdup
0.9 0.0% 100.0% 0.9 0.0% __res_context_send
0.7 0.0% 100.0% 0.7 0.0% JLI_MemAlloc
...
通過分析結果我們可以將目光聚焦在updatewindow
與Java_java_util_zip_Inflater_init
上,由於updatewindow
不是Java方法申請的內存,我們可以忽略不計,將重心放在Java Native 方法Java_java_util_zip_Inflater_init
上,根據這個結果我們可以去項目中搜索所有使用Inflater類的代碼,最終將範圍縮小到GZIPInputStream
與GZIPOutputStream
這兩個類,但是由於項目中使用這些類的地方還是比較多,所以依然無法確認問題代碼
定位問題代碼並解決
使用 gdb
命令 dump
出了那些64M的內存塊,然後通過查看dump出來的結果最終定位到問題
注意:dump內存會掛起應用進程,一定要確保沒有流量流入再使用
1、找出那些64MB內存的地址
#命令
less /proc/$pid/smaps
#結果
[www@idc06-c-marketingcenter-07 ~]$ less /proc/14626/smaps
7f43dc000000-7f440e000000 ---p 00000000 00:00 0
Size: 65536 kB
Rss: 65536 kB
Pss: 65044 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Anonymous: 65536 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd wr mr mw me ac sd
...
2、使用gdb
命令dump
出內存
#先連接程序
gdb -pid $pid
#進入gdb調試模式dump內存
dump memory mem.bin 7f43dc000000 7f440e000000
#mem.bin是內存dump出來的文件,後面是地址
strings mem.bin > mem.log #將二進制文件讀取成字符串並輸出到文件,方便查閱
less mem.log
內存dump文件內容如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GbawwRmb-1579961597080)(D:\Infomation\zzw\微信截圖_20190521154932.png)]
上圖省略了一部分內容,我發現很多個64MB文件全都是GroupProductItemAi
對象,最後終於肯定泄漏的對象就是 marketing-center
中的 POJO
com.mryt.marketing.center.domain.GroupProductItemAi
然後在項目中全局搜索該POJO
然後根據上面得到的GZIPInputStream
與GZIPOutputStream
直接申請內存等關鍵信息定位到最終的問題代碼
/**
* 問題代碼
*/
public Map<String, T> hgetAllObject(String key, int seconds) {
if (key == null) {
return null;
}
Map<byte[], byte[]> hMap = null;
ShardedJedis commonJedis = null;
Map<String, T> returnMap = null;
T object = null;
try {
commonJedis = jedisPool.getResource();
hMap = commonJedis.hgetAll(key.getBytes());
if (hMap == null) {
return null;
}
returnMap = new HashMap<String, T>();
Set<byte[]> keySet = hMap.keySet();
ByteArrayInputStream i = null;
GZIPInputStream gzin = null;
ObjectInputStream in = null;
// 這裏循環創建了i gzin in等三個對象的多個副本
for (Iterator<byte[]> it = keySet.iterator(); it.hasNext(); ) {
byte[] keyItem = it.next();
byte[] valueItem = hMap.get(keyItem);
// 建立字節數組輸入流
i = new ByteArrayInputStream(valueItem);
// 建立gzip解壓輸入流
gzin = new GZIPInputStream(i);
// 建立對象序列化輸入流
in = new ObjectInputStream(gzin);
// 按制定類型還原對象
object = (T) in.readObject();
returnMap.put(new String(keyItem), object);
}
// 這裏只釋放了最後一個,造成了中間對象沒有調用close方法釋放內存
if (i != null && gzin != null && in != null) {
i.close();
gzin.close();
in.close();
}
if (seconds > 0) {
commonJedis.expire(key, getRealCacheTime(seconds));
}
} catch (Exception e) {
jedisPool.returnBrokenResource(commonJedis);
RedisException.exceptionJedisLog(logger, key, commonJedis, e, "hgetAllObject");
commonJedis = null;
} finally {
if (commonJedis != null) {
jedisPool.returnResource(commonJedis);
}
}
return returnMap;
}
上面的代碼在for
循環裏創建了多個GZIPInputStream
但是卻只在for
循環之後釋放了最後一個GZIPInputStream
對象,所以造成了大量的內存泄漏,至此,終於找到了泄露源,然後根據業務情況選擇修復方式即可。