问题发现
年前,公司一个傻逼运维找到我,说我负责的两个服务经常出现 OOM kill 的情况,让我尽快溯源解决问题,维护系统稳定性。 于是乎,我打开了系统稳定性监控大盘,惊讶的发现,内存利用率高达 95% ,我心思拉一下之前的看看啥样吧,结果一看,一年内都是这么高(一年是监控大盘的极限),基本每两天就会 OOM 一次,emmm,系统能跑
排查过程
- 检查日志,查看是否有明显 OOM 异常抛出。但是没有任何异常日志
- 重启服务,发现系统内存利用率稳定增长,最终达到临界值 95% 左右,趋势平缓,几乎停止增长,略有波动
- 打印 dump 文件,并未发现可疑的由代码产生的 OOM 可疑点 事已至此,略微有些懵逼,不知道从何查起,毕竟第一次遇到 OOM 异常,没有任何经验,之前在简历中写过类似的经验,不过无非是其他同事排查到后,我抄的别人的,根本没有任何头绪,没办法,求助 AI,以下内容,由 AI 总结:
一、第一步:确认 OOM 类型
1️⃣ 看 Pod 状态
如果是 K8s:
sql
kubectl describe pod xxx
如果看到:
vbnet
Reason: OOMKilled
Exit Code: 137
那说明:
❗ 是容器被 cgroup 强制干掉
JVM 根本来不及抛异常
这种情况下,日志里没有 OOM 是正常的。
二、搞清楚 JVM 内存构成
很多人误以为:
ini
容器 1G
-Xmx512m
=> 没问题
这是典型误区。
JVM 内存 = 不只是堆。
你当时的启动参数:
diff
-Xms512m
-Xmx512m
-XX:MaxDirectMemorySize=256m
-XX:MaxMetaspaceSize=256m
-Xss512k
我们算一笔账:
| 内存区域 | 大小 |
|---|---|
| Heap | 512m |
| Direct | 256m |
| Metaspace | 256m |
| 线程栈(假设 300 线程) | 300 × 512k ≈ 150m |
| CodeCache | ~50m |
| JVM Native | ~50m |
| GC 结构 | ~几十 MB |
👉 实际可能接近 1.2G+
而你的容器之前是 1G。
结果?
💥 还没等堆 OOM,容器先被杀了。
三、如何判断是不是堆泄漏?
jstat -gc
ini
OU = 331540.3
OC = 349568.0
👉 Old 区使用率 ≈ 95%
并且:
- FGC 次数很少(只有 4 次)
- Old 使用量不下降
这很明显:
⚠️ 老年代对象回收不掉
这才是真正的堆泄漏特征。
四、标准排查流程(建议保存)
🧭 第一步:确认容器总内存
bash
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
确认 cgroup 限制。
🧭 第二步:看 JVM 真实内存占用
bash
ps -eo pid,rss,vsz,comm | grep java
看 RSS。
或者:
css
jcmd <pid> VM.native_memory summary
(前提开启 -XX:NativeMemoryTracking=summary)
🧭 第三步:判断是不是堆问题
方法一:jstat 连续观察
xml
jstat -gc <pid> 1000
看:
- Old 是否持续上涨
- FGC 后是否下降
如果:
sql
FGC 后 Old 不降
👉 基本确定是堆泄漏。
🧭 第四步:导出堆快照
这是关键步骤。
perl
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
然后下载到本地。
🧭 第五步:用 MAT 分析
用:
- Eclipse MAT
- VisualVM
看:
- Dominator Tree
- Retained Size
- 哪个类占用最多
一般常见元凶:
| 类型 | 场景 |
|---|---|
| HashMap | 缓存没清 |
| ThreadLocal | 忘 remove |
| List | 不断 add |
| 静态变量 | 永远不释放 |
| 业务缓存 | 没有上限 |
| MQ 未消费队列 | 堆积 |
五、除了堆泄漏,还有哪些坑?
这次是堆老年代满,扩展一下全场景。
1️⃣ DirectMemory 泄漏
特征:
- 堆不满
- 容器内存上涨
- 无 OOM 日志
常见场景:
- Netty
- NIO
- ByteBuffer.allocateDirect
- RocketMQ 客户端
排查:
ini
-XX:NativeMemoryTracking=summary
jcmd VM.native_memory summary
看 Direct 是否暴涨。
2️⃣ 线程爆炸
特征:
- 线程数持续上涨
- RSS 上涨
- Heap 正常
查看:
bash
jstack | grep "nid=" | wc -l
每个线程 512k ~ 1M。
3000 线程就 3G。
很多人死在这里。
3️⃣ Metaspace 泄漏
特征:
- 动态代理
- 频繁加载类
- 热部署框架
查看:
jstat -gcmetacapacity
4️⃣ GC 算法问题
比如:
- CMS 碎片
- G1 region 不均衡
- 晋升失败
可加:
ruby
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
或者 JDK11:
ruby
-Xlog:gc*
5️⃣ 内存抖动(流量型上涨)
有些情况不是泄漏。
比如:
- 瞬时大流量
- 批量任务
- 大对象 JSON 解析
特征:
- Old 会涨
- 但 FGC 后会降
这种不是泄漏。
六、根因推断
根据 jstat:
sql
Old 使用率 95%
FGC 不下降
我判断:
✅ 是典型老年代对象泄漏
❌ 不是 Direct
❌ 不是线程爆炸
❌ 不是 Metaspace
你升级到 2G 只是延缓爆炸时间。
本质问题还在。
七、工程级防御建议
这是我个人强烈建议做的:
✅ 1. 永远预留 30% 容器内存
容器 2G:
diff
-Xmx 1200m ~ 1400m
不要顶满。
✅ 2. 限制缓存大小
所有缓存必须:
- 有上限
- 有淘汰策略
推荐:
- Caffeine
- Guava Cache
✅ 3. 打开 OOM dump
ruby
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/
让问题可追溯。
✅ 4. 开启 NMT(生产可开 summary)
ini
-XX:NativeMemoryTracking=summary
成本极低。
✅ 5. 做线程池审计
- 不允许 newCachedThreadPool
- 所有线程池必须有界
以上就是 AI 帮我总结的这一部分经验 总结起来,这一次的经历中,有一些 AI 提供的 Linux 命令还是挺好的,比如以下几条:
diff
查看 Pod 状态(K8s)
kubectl describe pod <pod-name>
例如:
Last State:
Terminated
Reason: OOMKilled
Exit Code: 137
说明:
-137 = 被 cgroup 杀
-JVM 来不及抛 OutOfMemoryError
-属于容器内存超限,不一定是堆 OOM
bash
查看容器真实内存限制
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
作用:查看容器 cgroup 实际限制(单位字节)
diff
查看 Java 进程实际占用内存
ps -eo pid,rss,vsz,comm | grep java
字段说明:
-RSS:真实物理内存(单位 KB) ← 重点
-VSZ:虚拟内存
判断逻辑:
如果 RSS 接近容器限制 → 是整体内存超限
查看 JVM 堆使用情况
sql
实时监控 GC
jstat -gc <pid> 1000
每秒打印一次
重要字段说明:
| 字段 | 含义 |
| ------- | ------------ |
| EC / EU | Eden 容量 / 使用|
| OC / OU | Old 容量 / 使用 |
| YGC | Young GC 次数 |
| FGC | Full GC 次数 |
| FGCT | Full GC 总耗时 |
判断堆泄漏方法:
如果出现:
-OU 持续上涨
-FGC 后 OU 不下降
基本确认老年代泄漏
导出堆快照(关键步骤)
perl
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
作用:
生成堆内存快照文件
后续用:
-Eclipse MAT
-VisualVM
分析 Dominator Tree / Retained Size
diff
查看 JVM Native 内存(堆外)
前提:启动时加
-XX:NativeMemoryTracking=summary
查看命令:
jcmd <pid> VM.native_memory summary
输出会看到:
Heap
Class
Thread
Code
GC
Compiler
Internal
Symbol
Native Memory Tracking
Arena Chunk
Direct
重点看:
-Direct(堆外内存)
-Thread(线程栈)
-Class(Metaspace)
查看线程数量
bash
jstack <pid> | grep "nid=" | wc -l
或
top -H -p <pid>
说明:
-每个线程默认 512k ~ 1M
-1000 线程 ≈ 1G 内存
如果线程持续增长 → 线程泄漏
查看 Metaspace 使用情况
ini
jstat -gcmetacapacity <pid>
或者看:
jstat -gc <pid>
字段:
-MC / MU = Metaspace 容量 / 使用
如果 MU 持续上涨 → 类加载异常
查看对象统计(快速定位大类)
diff
jmap -histo <pid> | head -20
作用:
显示占内存最多的类
字段说明:
-instances:实例数量
-bytes:总占用字节
如果看到:
-HashMap 数量异常
-byte[] 特别大
-自定义类特别多
基本锁定方向
查看是否是 DirectMemory 泄漏
ini
jcmd <pid> VM.native_memory summary
看:
- Direct
(reserved=XXXMB, committed=XXXMB)
如果持续上涨 → Netty/NIO 问题
日志(强烈推荐) JDK8:
ruby
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-Xloggc:/data/gc.log
JDK11+:
-Xlog:gc*:file=/data/gc.log:time,uptime,level
作用:
判断:
-晋升失败
-老年代碎片
-Full GC 是否有效
自动生成 OOM Dump 生产必开:
ruby
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/
作用:
OOM 时自动生成堆文件
查看 JVM 参数
diff
jcmd <pid> VM.flags
或
jinfo -flags <pid>
作用:
确认:
--Xmx
-MaxDirectMemorySize
-MetaspaceSize
-GC 算法
查看 JVM 各区域配置
diff
jcmd <pid> GC.heap_info
作用:
查看:
- 新生代大小
- 老年代大小
- Region 情况(G1)
查看系统层内存
sql
free -m
top
cat /proc/meminfo
排除:
-其他进程占用
-系统缓存问题
常见问题定位对照表
| 现象 | 大概率原因 |
| ---------------- | ------------ |
| Old 持续上涨 | 堆泄漏 |
| Heap 正常,RSS 上涨 | DirectMemory |
| 线程数持续增加 | 线程泄漏 |
| Metaspace 涨 | 类加载异常 |
| GC 频繁但不释放 | 大对象/缓存失控 |
| 容器 OOMKilled 无异常 | cgroup 限制