Java應(yīng)用堆外內(nèi)存泄漏排查

背景

我司商城系統(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é)果我們可以將目光聚焦在updatewindowJava_java_util_zip_Inflater_init上,由于updatewindow不是Java方法申請的內(nèi)存试伙,我們可以忽略不計(jì)嘁信,將重心放在Java Native 方法Java_java_util_zip_Inflater_init上于样,根據(jù)這個(gè)結(jié)果我們可以去項(xiàng)目中搜索所有使用Inflater類的代碼,最終將范圍縮小到GZIPInputStreamGZIPOutputStream這兩個(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ù)上面得到的GZIPInputStreamGZIPOutputStream直接申請內(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ù)方式即可椒丧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末壹甥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子壶熏,更是在濱河造成了極大的恐慌句柠,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棒假,死亡現(xiàn)場離奇詭異溯职,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)帽哑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門谜酒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妻枕,你說我怎么就攤上這事僻族。” “怎么了屡谐?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵述么,是天一觀的道長。 經(jīng)常有香客問我愕掏,道長度秘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任亭珍,我火速辦了婚禮敷钾,結(jié)果婚禮上枝哄,老公的妹妹穿的比我還像新娘。我一直安慰自己阻荒,他們只是感情好挠锥,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著侨赡,像睡著了一般蓖租。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上羊壹,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天蓖宦,我揣著相機(jī)與錄音,去河邊找鬼油猫。 笑死稠茂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的情妖。 我是一名探鬼主播睬关,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毡证!你這毒婦竟也來了电爹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤料睛,失蹤者是張志新(化名)和其女友劉穎丐箩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恤煞,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屎勘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阱州。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挑秉。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖苔货,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情立哑,我是刑警寧澤夜惭,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站铛绰,受9級(jí)特大地震影響诈茧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捂掰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一敢会、第九天 我趴在偏房一處隱蔽的房頂上張望曾沈。 院中可真熱鬧,春花似錦鸥昏、人聲如沸塞俱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽障涯。三九已至,卻和暖如春膳汪,著一層夾襖步出監(jiān)牢的瞬間唯蝶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工遗嗽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粘我,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓痹换,卻偏偏與公主長得像涂滴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子晴音,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容