一次OOM排查解決過程(dump文件分析)

一、發(fā)現問題
近期 OOM 故障頻發(fā),一周內發(fā)生了 3 次。
但每次pod重啟后,應用又一切正常;上個版本有代碼發(fā)布的同學,排查了一遍新增代碼,沒找到可疑之處。
主要現象如下:
- 群里告警:Grafana 告警,Full GC 次數5分鐘內超2次
- 群里告警:nacos、grpc 等中間件心跳連接超時
- 集群中對應服務:重啟次數新增

Grafana 告警 Alertname: HK-5分鐘內GC次數大于2次 狀態(tài): 告警? 內容: HK最近10分鐘內的FullGC過多,當前值: 75.5,環(huán)境:prod,實例:10.20.56.36 應用: xxxx 時間: 2025-10-13 19:48:51 詳情: 告警詳情 來源: Grafana 地址
查看 Grafana 中 JVM 問題:

可為什么會導致重啟?
看看得:JVM 內部 OOM → 應用假死(線程卡死或頻繁 Full GC)→ 健康檢查失敗 → K8s 重啟 Pod
Tips:另外一種 kill
OOM Killed Pod:Kubernetes 節(jié)點的 Linux 內核 OOM Killer 殺掉了 Pod 進程(可能是 JVM 進程),通常是容器的內存限制被突破。
排查此問題困難之處
既然知道是 OOM,那就找對應 OOM 生成 dump 文件,分析即可。
運維側反饋來不及 dump 文件,就重啟了,導致dump文件丟失。
最后,那我又是如何得到這個 dump 文件的?
既然運維側不給力,那就只能靠平時多觀察下這個應用的情況,一有懷疑情況就找對應運維同學。
恰好,被我抓到一次,直接坐到運維同學旁邊,讓其幫我上容器幫我 dump 一份數據。
二、定位問題
2.1 定位問題:dump 文件分析
生成 dump 文件:找運維同學操作,進入對應 pod 容器中,執(zhí)行如下命令:
# 1、找到對應 PID: jps -l # 2、執(zhí)行,只想 dump 活躍的對象: jcmd 12345 GC.heap_dump -all=false /tmp/heap_20240602.hprof
分析工具:使用的是 IDEA 自帶的 Profiler
直接打開對應的 dump 文件,展示如下:

可以看到 byte[] 和 byte[][] 數據是 MySQL 里的結果數據:

選擇 byte[]:

圖中的 GC Root: Java Frame 表示:
- 在 Java 的垃圾回收(GC)機制中,GC Root 是一組特殊的對象引用,它們作為起點,GC 會從這些對象開始遍歷引用鏈,找出所有可達對象。可達的對象不會被回收。
- Java Frame 指的是某個線程的棧幀(方法調用棧)中的局部變量或參數引用了這個對象。
GC Root: Java Frame: com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:951)
這段話意思是:
- 這個
byte[]對象是被某個線程的棧上的局部變量引用著。 - 具體是在
com.mysql.cj.protocol.a.NativeProtocol類的sendQueryPacket方法(第 951 行)中。 - 因為它在棧上被引用,所以 GC 無法回收它,直到這個方法執(zhí)行結束并且棧幀被銷毀。
找到一個具體的進入看下:

可以定位到某一個 SQL,并將這個 SQL 展示出來。
SELECT amount, currency FROM risk_message
所有的線索都指向這個 SQL 查詢帶出來大量的數據。
2.2 定位問題:具體代碼
通過 SQL 可以縮小范圍,所有涉及這個表的代碼,主要是 2 個接口:
- 入賬審核列表:getInboundList
- 匯總-入賬成功金額:querySummary —— BUG 點
代碼如下:
List<RiskMessage> list = riskMessageRepo.lambdaQuery().select(RiskMessage::getAmount, RiskMessage::getCurrency)
.eq(StringUtils.isNotBlank(clientId), RiskMessage::getClientId, clientId)
.in(CollectionUtils.isNotEmpty(currencyList), RiskMessage::getCurrency, currencyList)
.ge(Objects.nonNull(orderStartTime), RiskMessage::getValueDate, orderStartTime)
.le(Objects.nonNull(orderEndTime), RiskMessage::getValueDate, orderEndTime)
.list();這個功能主要匯總金額,但幣種不同,得按照匯率換算出來,他這塊實現步驟:
- 查詢出所有符合條件的 <金額、幣種> —— 問題點
- 在內存本地聚合
- 根據匯率進行換算,得出 USD 幣種的金額
問題就在查詢這塊,沒兜住,直接查詢出 百萬條 記錄,導致內存在接下來的 30分鐘 逐漸被占滿。
- 時間范圍沒生效:沒有強制時間范圍
- 按幣種先匯總:幣種只有百來個,返回也只有百來行
直接讓 AI 幫我排查這 2 個接口是否有問題:
claude-4-sonnet 回答道:

AI 的回答,居然是沒有問題;當再次指出問題時,AI 又站起來了。
三、小結
排查過程中發(fā)現的一些事:
- HTTP 請求調用時間長,不一定造成 OOM,但一定是有問題的。
- MySQL IN 數量能調節(jié),可以 1w+
- 現階段的AI編程,不能完全相信,會繞進一些BUG中,需要人工處理。
最后解決這個問題也比較簡單:
- 強制選定范圍
- 按照幣種 SUM,返回行數最多百來行
常見 Full GC 觸發(fā)原因:
| 觸發(fā)原因 | 說明 | 典型特征 | 排查方法 |
|---|---|---|---|
| 老年代空間不足 | 大對象直接進入老年代,或晉升失敗 | GC 日志顯示 Allocation Failure,老年代使用率接近 100% | 查看 GC 日志中 Old Gen 使用率,分析對象生命周期 |
| 元空間(Metaspace)不足 | 類加載過多,動態(tài)生成類(如反射、CGLIB) | GC 日志顯示 Metadata GC Threshold | -XX:MaxMetaspaceSize 設置過小,或類加載泄漏 |
顯式調用 System.gc() | 代碼或第三方庫調用 | GC 日志顯示 System.gc() | GC 日志會顯示 System.gc() 觸發(fā) |
| 直接內存不足 | DirectByteBuffer / Netty 堆外內存耗盡 | 堆外內存不足時 JVM 會頻繁 Full GC 嘗試釋放 Cleaner | -XX:MaxDirectMemorySize,用 jcmd VM.native_memory summary 查看 |
| 大對象分配失敗 | 超過 PretenureSizeThreshold 直接進老年代 | GC 日志顯示 Promotion failed | 調整閾值或優(yōu)化對象分配 |
| CMS/G1 的 remark 階段失敗 | 并發(fā)回收失敗,退化為 Full GC | GC 日志顯示 concurrent mode failure 或 to-space exhausted | 查看 GC 日志的 concurrent mode failure 或 to-space exhausted |
到此這篇關于一次OOM排查解決過程的文章就介紹到這了,更多相關OOM排查內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java:無法訪問org.springframework.boot.SpringApplication的解決方法
這篇文章主要給大家介紹了關于java:無法訪問org.springframework.boot.SpringApplication的解決方法,文中通過實例代碼將解決的辦法介紹的非常詳細,需要的朋友可以參考下2023-01-01

