Java內(nèi)存泄漏早期排查流程(未觸發(fā)頻繁Full GC前)
內(nèi)存泄漏早期最典型特征:老年代/堆內(nèi)存呈緩慢、持續(xù)、不可逆的上漲,但還沒嚴(yán)重到頻繁Full GC、OOM。
這個(gè)階段最容易定位根因,核心思路是:監(jiān)控趨勢(shì) → 定位泄漏區(qū)域 → 抓取堆快照 → 分析泄漏對(duì)象 → 定位代碼。
下面給你一套生產(chǎn)環(huán)境可直接落地的排查流程,從簡單到深入,不影響業(yè)務(wù)。
一、第一步:先確認(rèn)「真的是內(nèi)存泄漏」
先排除正常內(nèi)存使用、堆設(shè)置過小等情況。
1. 看內(nèi)存增長曲線(最關(guān)鍵)
通過監(jiān)控看 堆內(nèi)存/老年代 趨勢(shì):
- 正常:GC 后內(nèi)存會(huì)大幅回落,曲線呈鋸齒狀
- 泄漏:每次GC后回落很少,整體持續(xù)緩慢上漲
2. 快速命令驗(yàn)證(無需工具)
jstat -gcutil <pid> 1000 10 # 每1秒輸出一次GC情況,共10次
重點(diǎn)看 3 個(gè)指標(biāo):
O:老年代使用率FGC:Full GC 次數(shù)FGCT:Full GC 總耗時(shí)
泄漏早期特征:
O持續(xù)緩慢上升(比如從 30% → 40% → 50%)FGC很少(0~幾次)- 每次Full GC后,老年代下降幅度極小
只要滿足「老年代只漲不跌」,基本就是內(nèi)存泄漏。
二、第二步:定位泄漏類型(區(qū)分堆內(nèi)/堆外)
絕大多數(shù)泄漏是 堆內(nèi)泄漏,極少數(shù)是堆外。
1. 堆內(nèi)泄漏(95%場(chǎng)景)
- 特征:老年代持續(xù)上漲
- 原因:對(duì)象被長生命周期引用持有(靜態(tài)集合、線程池、緩存、單例)
2. 堆外泄漏
- 特征:堆內(nèi)存正常,但進(jìn)程物理內(nèi)存持續(xù)漲
- 原因:NIO DirectByteBuffer、JNI、池化資源未釋放
先按堆內(nèi)泄漏排查,效率最高。
三、第三步:無侵入抓取堆快照(核心步驟)
不需要重啟、不需要壓測(cè)、低影響,生產(chǎn)直接用。
推薦抓取時(shí)機(jī)
- 一次Full GC之后立即抓(最干凈,能精準(zhǔn)定位泄漏對(duì)象)
- 或者內(nèi)存漲到相對(duì)高位時(shí)抓
抓取命令
jmap -dump:format=b,file=leak.hprof <pid>
- 生成
leak.hprof二進(jìn)制堆轉(zhuǎn)儲(chǔ)文件 - 大小≈當(dāng)前堆使用量,注意磁盤空間
小提示:JDK8+ 加 -dump:live 可以只抓存活對(duì)象,文件更?。?/p>
jmap -dump:live,format=b,file=leak.hprof <pid>
四、第四步:用工具分析泄漏(10分鐘定位代碼)
最常用、最穩(wěn)定的工具:Eclipse MAT(免費(fèi)、強(qiáng)大)
1. MAT 打開 hprof 文件
選擇 Acquire Heap Dump / Open Heap Dump
2. 直接使用「泄漏嫌疑報(bào)告」
MAT 會(huì)自動(dòng)生成:Leak Suspects Report
它會(huì)直接告訴你:
- 哪個(gè)類占用內(nèi)存最大
- 被誰引用
- 哪行代碼導(dǎo)致泄漏
3. 核心分析 3 步
- Histogram(直方圖)
看對(duì)象實(shí)例數(shù),找數(shù)量異常多的業(yè)務(wù)對(duì)象(你的自定義類) - Merge Shortest Paths to GC Roots
右鍵對(duì)象 → 查看 GC Root 引用鏈 - 排除弱引用/軟引用
只看 強(qiáng)引用鏈,泄漏一定在這里
泄漏對(duì)象的典型特征
- 你的業(yè)務(wù)實(shí)體類(User/Order/Config等)實(shí)例數(shù)只增不減
- 被 static Map/List/Set 持有
- 被 線程池 / 單例 / 緩存 持有
- 被 ThreadLocal 持有(高頻泄漏點(diǎn))
五、第五步:早期泄漏最常見的 5 種代碼根因
你在分析時(shí),優(yōu)先排查這 5 類代碼,90% 泄漏都在這里:
1. 靜態(tài)集合(最常見)
private static List<Object> list = new ArrayList<>(); // 只add不remove,對(duì)象永遠(yuǎn)無法回收
2. ThreadLocal 未清理
private static ThreadLocal<Object> tl = new ThreadLocal<>(); // 線程池中的線程長期存活,ThreadLocalMap 一直持有對(duì)象
3. 線程池/自定義線程忘記關(guān)閉
- 線程是 GC Root,線程里的引用全部泄漏
- 線程池任務(wù)里持有大對(duì)象,且任務(wù)無限等待
4. 緩存未設(shè)置過期/淘汰
private static Map<String, Object> CACHE = new HashMap<>(); // 無過期、無淘汰,無限增長
5. IO/連接未關(guān)閉
- InputStream、Socket、Connection 未 close
- 底層持有堆外/堆內(nèi)引用
六、無工具、命令行快速初篩(應(yīng)急用)
如果不能用MAT,可以先用命令粗定位:
jmap -histo <pid> | head -20
輸出:實(shí)例數(shù)、字節(jié)數(shù)、類名
找自定義類、實(shí)例數(shù)異常高的類,就是懷疑對(duì)象。
七、最佳實(shí)踐:提前發(fā)現(xiàn)早期泄漏
不用等問題爆發(fā),配置監(jiān)控即可:
- 監(jiān)控 老年代使用率趨勢(shì)
- 監(jiān)控 GC后內(nèi)存回落幅度
- 配置告警:老年代連續(xù) 10 次 GC 不下降
- 看趨勢(shì):老年代只漲不跌 = 泄漏
- 抓快照:Full GC 后 jmap 抓 hprof
- 用MAT:看 Leak Suspects
- 查引用:靜態(tài)集合、ThreadLocal、線程池、緩存
- 看代碼:誰長期持有對(duì)象不釋放
以上就是Java內(nèi)存泄漏早期排查流程(未觸發(fā)頻繁Full GC前)的詳細(xì)內(nèi)容,更多關(guān)于Java內(nèi)存泄漏早期排查的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
手把手教你用Java實(shí)現(xiàn)一套簡單的鑒權(quán)服務(wù)
現(xiàn)今大部分系統(tǒng)都會(huì)有自己的鑒權(quán)服務(wù),本文介紹了最常用的鑒權(quán)服務(wù),就是日常用戶的登錄登出,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
Spring WebSocket 404錯(cuò)誤的解決方法
這篇文章主要為大家詳細(xì)介紹了Spring WebSocket 404錯(cuò)誤的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12
如何解決IDEA使用Tomcat控制臺(tái)中文出現(xiàn)亂碼問題
這篇文章主要介紹了如何解決IDEA使用Tomcat控制臺(tái)中文出現(xiàn)亂碼問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
詳解如何用spring Restdocs創(chuàng)建API文檔
這篇文章將帶你了解如何用spring官方推薦的restdoc去生成api文檔。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
Java線程重復(fù)執(zhí)行以及操作共享變量的代碼示例
這篇文章主要介紹了Java中對(duì)線程重復(fù)執(zhí)行以及操作共享變量的代碼示例,來自于Java面試題目的練習(xí)整理,需要的朋友可以參考下2015-12-12
mybatis中oracle實(shí)現(xiàn)分頁效果實(shí)例代碼
實(shí)現(xiàn)分頁的方式有很多,但常用的是通過SQL來顯示分頁。這篇文章主要介紹了mybatis中oracle實(shí)現(xiàn)分頁效果實(shí)例代碼,有興趣的可以了解一下。2017-04-04

