内存占用高排查实战:从 free 到 MAT,揪出那个吃内存的家伙
凌晨三点,手机震动,Prometheus 告警:「服务器可用内存低于 10%」。你睡眼惺忪地打开电脑,心里默念:「别是 OOM Killer,别是 OOM Killer……」然后
dmesg一看——果然,Java 进程已经被内核一枪毙了。本文记录一套完整的 Linux + Java 内存占用高排查流程:从
free到top,从jstat到 MAT,一步步定位内存泄漏的完整 SOP。不求背下来,但求下次被叫醒时,能少骂两句。
一、怎么发现内存高了
大部分人发现内存高,无非三种途径:
- Prometheus + Grafana 告警:监控大盘的
node_memory_MemAvailable_bytes跌破阈值,钉钉/飞书/微信群开始轰炸。 - 巡检发现:
free -h一看,used 99%,心跳加速。 - 进程被杀:最惨的情况——OOM Killer 已经替你做了决定,
dmesg | grep -i oom看到一行冰冷的日志:Out of memory: Killed process 12345 (java)。
但请注意一个关键常识:Linux 下 free 命令显示的高内存使用率,不一定是问题。
$ free -h
total used free shared buff/cache available
Mem: 15G 12G 512M 256M 2.8G 2.5G
Swap: 2.0G 128M 1.9G
很多新手看到 used 12G,立刻慌了。但实际上 Linux 会把空闲内存拿来做文件缓存(buff/cache),这是好事,不是泄漏。真正要看的是 available 这一列——它才代表「应用程序还能申请到多少内存」。
记住:free 很小不可怕,available 很小才可怕。Linux 的哲学是「内存闲着也是闲着,不如拿来当缓存」。
二、Linux 层定位:谁在吃内存
确认 available 确实低了之后,下一步就是找出那个「大胃王」进程。
1. free -h 整体视图
先搞清楚 free -h 每一列的含义:
| 字段 | 含义 | 说明 |
|---|---|---|
| total | 总物理内存 | 服务器硬件配置 |
| used | 已使用内存 | 包含了 buff/cache,数值偏大 |
| free | 完全空闲内存 | 没被任何人碰过的内存 |
| shared | 共享内存 | 主要是 tmpfs 使用 |
| buff/cache | 缓冲区/缓存 | 内核用来加速 I/O 的,可回收 |
| available | 可用内存 | 真正的剩余可用量,看这个 |
一句话总结:别看 free,看 available。available = free + 可回收的 buff/cache。
2. top 按内存排序
找到是哪个进程在吃内存:
# 启动 top 后按 M 键,按内存使用排序
top
# 进入后按 Shift + M
关注三列内存指标:
| 列名 | 全称 | 含义 |
|---|---|---|
| VIRT | Virtual Memory | 虚拟内存,进程申请的总量(包含未实际分配的) |
| RES | Resident Memory | 常驻内存,实际占用的物理内存,重点看这个 |
| SHR | Shared Memory | 共享内存,如共享库 .so 文件占用的部分 |
VIRT 大不要慌,Java 的 VIRT 天生就大(虚拟地址空间预留)。RES 才是真正吃掉的物理内存。如果某个 Java 进程 RES 占了 10G+,那它就是嫌疑犯。
3. /proc/PID/status 精确查看
锁定了可疑 PID 之后,精确查看它的内存使用:
# 替换 PID 为实际进程 ID
cat /proc/PID/status | grep -E 'VmRSS|VmSize|VmSwap|Name'
输出示例:
Name: java
VmSize: 8234560 kB # 虚拟内存大小
VmRSS: 6123456 kB # 常驻物理内存(约 5.8G)
VmSwap: 12340 kB # 被换出到 swap 的部分
| 指标 | 说明 |
|---|---|
| VmRSS | 实际物理内存占用,最重要的指标 |
| VmSize | 虚拟内存,通常比 RSS 大很多,不用太紧张 |
| VmSwap | 被换到磁盘的内存,如果这个值很大,说明内存真的不够了 |
4. smaps 查看内存映射
如果需要更细粒度地了解内存分布:
# 查看进程的内存映射摘要
cat /proc/PID/smaps | grep -i rss | awk '{sum+=$2} END {print sum/1024 " MB"}'
# 或者用 smaps_rollup(内核 4.14+)更方便
cat /proc/PID/smaps_rollup
这一步通常用于排查「非堆内存」泄漏,比如 Native 内存、mmap 映射等场景。
三、Java 层定位:JVM 内存到底花在哪
到这一步,我们已经确认了是某个 Java 进程在吃内存。接下来要进入 JVM 内部,看看钱(内存)花在了哪里。
1. jstat 看 GC 和堆使用
jstat 是 JDK 自带的轻量级监控工具,线上可以放心用,开销极小:
# 每秒打印一次 GC 统计,持续观察
jstat -gc PID 1000
输出列含义速查:
| 列 | 含义 | 单位 |
|---|---|---|
| S0C / S1C | Survivor 0/1 容量 | KB |
| S0U / S1U | Survivor 0/1 已使用 | KB |
| EC / EU | Eden 区容量 / 已使用 | KB |
| OC / OU | Old 区容量 / 已使用 | KB |
| MC / MU | Metaspace 容量 / 已使用 | KB |
| YGC / YGCT | Young GC 次数 / 耗时 | 次 / 秒 |
| FGC / FGCT | Full GC 次数 / 耗时 | 次 / 秒 |
关键判断逻辑:
- OU(Old 区使用量)持续上升,Full GC 后也不下降 --> 大概率堆内存泄漏
- FGC 频繁,FGCT 耗时越来越长 --> 堆快满了,GC 在做垂死挣扎
- MU 持续上升 --> 可能是 Metaspace 泄漏(动态类加载过多)
# 也可以看百分比,更直观
jstat -gcutil PID 1000
如果你看到 Old 区使用率 99%,Full GC 次数几百次——恭喜,你找到了一个正在被内存泄漏慢慢掐死的 JVM。
2. jmap 导出堆转储
确认堆内存有问题后,需要导出堆转储文件(Heap Dump)做离线分析:
# 导出堆转储
jmap -dump:format=b,file=/tmp/heap.hprof PID
WARNING:线上执行 jmap dump 会触发 STW(Stop The World),期间所有业务线程暂停。堆越大,暂停时间越长。4G 堆大概暂停几秒到十几秒,8G+ 可能更久。
最佳实践:不要等出事了再手动 dump,在 JVM 启动参数里提前配置:
# JVM 启动参数中加入,OOM 时自动 dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heap-dump.hprof
这样 OOM 的时候会自动生成 dump 文件,不用你凌晨三点爬起来手动操作——毕竟你的手可能在抖。
3. MAT 分析堆转储
拿到 .hprof 文件后,用 Eclipse MAT(Memory Analyzer Tool)打开分析。MAT 是 Java 内存分析的瑞士军刀,免费且强大。
分析三板斧:
第一斧:Leak Suspects Report(泄漏嫌疑报告)
打开 hprof 文件后,MAT 会自动生成泄漏嫌疑报告。它会告诉你:
- 哪个对象占了最多内存
- 从 GC Root 到该对象的引用链
- 嫌疑对象的类名和大小
大部分情况下,看完这个报告就能定位问题了。
第二斧:Dominator Tree(支配树)
Dominator Tree 按「支配关系」排列对象,从大到小。谁持有的内存最多,谁就排在最前面。
操作路径:MAT 菜单 --> Open Dominator Tree
通常你会看到某个 HashMap、ArrayList 或者自定义缓存类霸占了 80% 以上的堆内存。点进去看它持有的元素数量——如果一个 HashMap 里有几百万条数据,那它就是「凶手」。
第三斧:Top Consumers(大对象排行榜)
按类或包维度,展示内存消耗 Top N。适合快速定位是哪个模块、哪个包在吃内存。
4. Arthas 在线诊断
如果不方便做堆 dump(比如堆太大,dump 会影响业务),可以用阿里开源的 Arthas 做在线诊断:
# 启动 Arthas,附加到目标 Java 进程
java -jar arthas-boot.jar
# 查看 JVM 整体状态
dashboard
# 导出堆转储(比 jmap 更安全,支持只 dump 存活对象)
heapdump --live /tmp/heap.hprof
# 查看某个类的实例数量和占用内存
vmtool --action getInstances --className java.util.HashMap --limit 10
# 查看某个对象的详细信息
vmtool --action getInstances --className com.xxx.CacheManager --express 'instances[0].cache.size()'
Arthas 的
dashboard命令堪称「JVM 体检报告」,一个命令看到堆使用、GC 情况、线程状态,非常适合第一时间快速摸底。
四、常见 5 大原因 + 解法
排查了这么多次内存问题,总结下来逃不出这五大类:
1. 堆内泄漏:HashMap / List 只增不减
典型场景:
- 用
static Map做本地缓存,只 put 不 remove,数据越攒越多 ThreadLocal用完没有remove(),线程池复用导致数据一直留着- 事件监听器注册了但从不注销
// 反面教材:静态 Map 当缓存,永不过期
private static final Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
cache.put(key, value); // 只进不出,内存迟早爆
}
解决方案:
- 本地缓存用 Caffeine 或 Guava Cache,设置最大容量和过期时间
ThreadLocal用完一定要remove()- 使用
WeakHashMap或WeakReference让 GC 能回收
// 正面教材:用 Caffeine 做有界缓存
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000) // 最多 1 万条
.expireAfterWrite(10, TimeUnit.MINUTES) // 10 分钟过期
.build();
2. 大对象:一次查询返回百万行
典型场景:
SELECT * FROM t_order不加LIMIT,百万行数据全部加载到内存- 读取整个大文件到
byte[] - 一次 RPC 调用返回的列表没有分页
解决方案:
- 数据库查询必须加 LIMIT,或使用流式查询(
fetchSize) - 大文件用
BufferedReader逐行读取,不要Files.readAllBytes() - 接口返回数据做分页,设置合理的单页上限
// 反面教材
List<Order> orders = orderMapper.selectAll(); // 100 万条全加载
// 正面教材:流式查询
try (Cursor<Order> cursor = orderMapper.selectAllWithCursor()) {
cursor.forEach(order -> processOrder(order));
}
3. 堆外泄漏:DirectByteBuffer / Netty
典型场景:
- NIO 的
DirectByteBuffer分配后没有显式释放 - Netty 的
ByteBuf忘记调用release() - 使用了 JNI / Native 库存在内存泄漏
解决方案:
# 限制 Direct Memory 最大值,避免无限膨胀
-XX:MaxDirectMemorySize=512m
# Netty 开启泄漏检测(测试环境用 PARANOID,生产用 SIMPLE)
-Dio.netty.leakDetection.level=PARANOID
堆外泄漏的麻烦之处在于:jmap 看不到,MAT 也分析不出来,因为这些内存不在 JVM 堆里。需要用 NMT(Native Memory Tracking)或 pmap 来排查:
# 启用 NMT(JVM 启动参数)
-XX:NativeMemoryTracking=detail
# 运行时查看 Native 内存分布
jcmd PID VM.native_memory detail
4. 元空间溢出:Metaspace
典型场景:
- 大量使用动态代理(CGLib、Javassist),每次都生成新的类
- Groovy / JSP / 脚本引擎频繁编译,类加载器不断创建
- OSGi 或热部署框架的类加载器泄漏
报错信息:
java.lang.OutOfMemoryError: Metaspace
解决方案:
# 限制 Metaspace 大小,避免无限增长
-XX:MaxMetaspaceSize=256m
# 监控已加载的类数量
jstat -gc PID 1000 # 看 MC/MU 列
jcmd PID GC.class_stats # 查看类统计
5. GC 参数不合理
典型场景:
- 堆设置太小,业务量上来后频繁 Full GC
- 使用了不适合当前场景的 GC 收集器
- 新生代和老年代比例不合理
GC 选择参考:
| 场景 | 推荐 GC | JVM 参数 |
|---|---|---|
| 小堆(< 4G),低延迟 | G1 | -XX:+UseG1GC |
| 大堆(4G~16G),吞吐优先 | G1 | -XX:+UseG1GC -XX:MaxGCPauseMillis=200 |
| 超大堆(16G+),极低延迟 | ZGC | -XX:+UseZGC(JDK 17+) |
| 批处理/离线任务,吞吐优先 | Parallel | -XX:+UseParallelGC |
堆大小经验公式:
# 一般建议:堆大小 = 存活对象大小 × 3~4 倍
# 如果 Full GC 后老年代还剩 1G 存活对象,堆至少给 3~4G
-Xms4g -Xmx4g # 最小值和最大值设一样,避免动态扩缩容的开销
五、Java 内存区域速查表
JVM 的内存不只有堆,出问题的时候要知道是哪个区域炸了:
| 内存区域 | 存放内容 | OOM 报错信息 | 排查工具 |
|---|---|---|---|
| 堆(Heap) | 对象实例 | java.lang.OutOfMemoryError: Java heap space |
jmap + MAT |
| 元空间(Metaspace) | 类元数据、方法信息 | java.lang.OutOfMemoryError: Metaspace |
jstat -gc |
| 直接内存(Direct) | NIO 缓冲区 | java.lang.OutOfMemoryError: Direct buffer memory |
NMT / jcmd |
| 线程栈(Stack) | 方法调用栈帧 | StackOverflowError |
jstack |
| Native 内存 | JNI、C/C++ 库 | 进程被 OOM Killer 杀死 | pmap、smaps |
最阴险的是 Native 内存泄漏——JVM 自己的监控工具看不到,只有 Linux 层面才能发现。进程 RES 持续涨,但 JVM 堆使用率不高?十有八九是堆外泄漏。
六、排查 SOP 速查表
下次被叫醒时,照着这个表一步步来:
| 步骤 | 命令 | 目的 |
|---|---|---|
| 1 | free -h |
确认是否真的内存不足(看 available) |
| 2 | top(按 M 排序) |
找到吃内存最多的进程 PID |
| 3 | cat /proc/PID/status |
确认进程的 RSS、Swap 使用 |
| 4 | jstat -gcutil PID 1000 |
查看 JVM 堆各区使用率和 GC 频率 |
| 5 | jmap -dump:format=b,file=/tmp/heap.hprof PID |
导出堆转储(注意 STW 风险) |
| 6 | MAT 打开 hprof | 分析 Leak Suspects + Dominator Tree |
| 7 | jcmd PID VM.native_memory detail |
排查堆外内存(如 NMT 已开启) |
快捷版(一行命令快速摸底):
# 看内存总览
free -h && echo "---" && top -b -o %MEM -n 1 | head -20
# 看 Java 进程的 GC 情况(替换 PID)
jstat -gcutil PID 1000 5
# 看进程内存详情(替换 PID)
cat /proc/PID/status | grep -E 'Name|VmRSS|VmSize|VmSwap|Threads'
七、总结
内存泄漏就像家里的水管漏水——滴滴答答的时候你根本注意不到,等你发现的时候,地板已经泡烂了。
核心要点回顾:
- Linux 层:看
available而不是free,用top+/proc/PID/status定位吃内存的进程 - JVM 层:
jstat看 GC 趋势,jmap导出堆转储,MAT 分析泄漏嫌疑 - 堆外内存:NMT +
pmap是唯一的救命稻草,堆监控看不到的泄漏最危险 - 预防大于治疗:启动参数加上
-XX:+HeapDumpOnOutOfMemoryError,监控加上内存告警,本地缓存用有界的 Caffeine/Guava - 五大常见原因:堆内泄漏、大对象、堆外泄漏、Metaspace 溢出、GC 参数不合理
最后的忠告:每一个线上 OOM 的背后,都有一个程序员曾经说过「这个 Map 不会很大的」。
如果这篇文章帮你少被叫醒了一次,那它就值了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)