背景
我司商城系統(tǒng)生產(chǎn)服務(wù)隔一段時(shí)間就掛掉一次,所有的機(jī)器都有這個(gè)問題彬向,而且問題出現(xiàn)的越來越頻繁兼贡,從最開始的半個(gè)月一次,到后來一周一次娃胆、3天一次遍希,一直到最后的1天1次甚至2次,導(dǎo)致服務(wù)極其不穩(wěn)定里烦,查找泄漏源成了迫切要解決的問題
初步排查和猜測
1凿蒜、首先獲取應(yīng)用pid
ps -ef|grep marketing-center
2、根據(jù)pid查詢java應(yīng)用堆內(nèi)存使用情況胁黑,以及應(yīng)用進(jìn)程占用系統(tǒng)內(nèi)存情況
#查看java程序GC情況以及堆內(nèi)內(nèi)存使用情況
jstat -gc $pid 2000
#結(jié)果如下
[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
#查看應(yīng)用進(jìn)程占用系統(tǒng)內(nèi)存情況废封,可以用top代替
ps -p $pid -o rss,vsz
#結(jié)果如下:
[www@idc06-c-marketingcenter-03 ~]$ ps -p 14626 -o rss,vsz
RSS VSZ
12303720 19423396
根據(jù)以上信息可以看出,堆內(nèi)內(nèi)存使用也就5G左右别厘,但是Java應(yīng)用實(shí)際占用內(nèi)存卻高達(dá)12G左右虱饿,而且堆內(nèi)內(nèi)存一切正常拥诡,Young GC也比較正常平穩(wěn)触趴,F(xiàn)ull GC也保持在一個(gè)較低的頻率,通過以上數(shù)據(jù)基本可以斷定發(fā)生了Java堆外內(nèi)存泄漏渴肉,為了驗(yàn)證我的猜想冗懦,使用pmap命令查看一下系統(tǒng)內(nèi)存分配
#查看系統(tǒng)內(nèi)存分配情況
pmap -x $pid | sort -k3 -n
#結(jié)果如下,內(nèi)容較多仇祭,截取關(guā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 ]
...
發(fā)現(xiàn)大量64MB左右的內(nèi)存塊披蕉,且分配地址在堆外,以上內(nèi)容驗(yàn)證了我的猜想乌奇,確實(shí)有堆外內(nèi)存泄漏
詳細(xì)排查經(jīng)過
由于是堆外內(nèi)存泄漏没讲,JDK自帶的工具已經(jīng)不好用了,首先借助谷歌的內(nèi)存分配監(jiān)測工具gperftools
來排查具體哪段代碼進(jìn)行了堆外內(nèi)存申請礁苗,具體安裝使用請參考:Java直接內(nèi)存泄漏排查工具gperftools使用方法
#結(jié)果較多爬凑,截取部分
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
...
通過分析結(jié)果我們可以將目光聚焦在updatewindow
與Java_java_util_zip_Inflater_init
上,由于updatewindow
不是Java方法申請的內(nèi)存试伙,我們可以忽略不計(jì)嘁信,將重心放在Java Native 方法Java_java_util_zip_Inflater_init
上于样,根據(jù)這個(gè)結(jié)果我們可以去項(xiàng)目中搜索所有使用Inflater類的代碼,最終將范圍縮小到GZIPInputStream
與GZIPOutputStream
這兩個(gè)類潘靖,但是由于項(xiàng)目中使用這些類的地方還是比較多穿剖,所以依然無法確認(rèn)問題代碼
定位問題代碼并解決
使用 gdb
命令 dump
出了那些64M的內(nèi)存塊,然后通過查看dump出來的結(jié)果最終定位到問題
注意:dump內(nèi)存會(huì)掛起應(yīng)用進(jìn)程卦溢,一定要確保沒有流量流入再使用
1糊余、找出那些64MB內(nèi)存的地址
#命令
less /proc/$pid/smaps
#結(jié)果
[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
出內(nèi)存
#先連接程序
gdb -pid $pid
#進(jìn)入gdb調(diào)試模式dump內(nèi)存
dump memory mem.bin 7f43dc000000 7f440e000000
#mem.bin是內(nèi)存dump出來的文件单寂,后面是地址
strings mem.bin > mem.log #將二進(jìn)制文件讀取成字符串并輸出到文件啄刹,方便查閱
less mem.log
內(nèi)存dump文件內(nèi)容如下:
[www@idc06-c-marketingcenter-03 ~]$ less mem.log
...
com.xxx.marketing.domain.dto.GroupProductItemAi&$*^@($)-jtsjrns?:]][\\..,;0&^(
=-com.xxx.marketing.domain.dto.GroupProductItemAi&$*^@($)-jtsjrns?:]][\\..,;0&^(
=-com.xxx.marketing.domain.dto.GroupProductItemAi&$*^@($)-jtsjrns?:]][\\..,;0&^(
=-&$*^@($)-jtsjrns?:]][\\..,;0&^(=-com.xxx.marketing.domain.dto.GroupProductItemAi
...
上圖省略了一部分內(nèi)容,我發(fā)現(xiàn)很多個(gè)64MB文件全都是GroupProductItemAi
對象凄贩,最后終于肯定泄漏的對象就是 marketing-center
中的 POJO
com.mryt.marketing.center.domain.GroupProductItemAi
然后在項(xiàng)目中全局搜索該POJO
然后根據(jù)上面得到的GZIPInputStream
與GZIPOutputStream
直接申請內(nèi)存等關(guān)鍵信息定位到最終的問題代碼
/**
* 問題代碼
*/
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;
// 這里循環(huán)創(chuàng)建了i gzin in等三個(gè)對象的多個(gè)副本
for (Iterator<byte[]> it = keySet.iterator(); it.hasNext(); ) {
byte[] keyItem = it.next();
byte[] valueItem = hMap.get(keyItem);
// 建立字節(jié)數(shù)組輸入流
i = new ByteArrayInputStream(valueItem);
// 建立gzip解壓輸入流
gzin = new GZIPInputStream(i);
// 建立對象序列化輸入流
in = new ObjectInputStream(gzin);
// 按制定類型還原對象
object = (T) in.readObject();
returnMap.put(new String(keyItem), object);
}
// 這里只釋放了最后一個(gè)誓军,造成了中間對象沒有調(diào)用close方法釋放內(nèi)存
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
循環(huán)里創(chuàng)建了多個(gè)GZIPInputStream
但是卻只在for
循環(huán)之后釋放了最后一個(gè)GZIPInputStream
對象,所以造成了大量的內(nèi)存泄漏疲扎,至此昵时,終于找到了泄露源,然后根據(jù)業(yè)務(wù)情況選擇修復(fù)方式即可椒丧。