Java 内存溢出(OOM)排查实战指南:从复现到 MAT Dump 分析

Java 内存溢出(OOM)排查实战指南:从复现到 MAT Dump 分析

内存溢出(OutOfMemoryError,简称 OOM)是 Java 开发中最常见的线上问题之一,几乎所有 Java 工程师都会遇到。它通常发生在堆(Heap)、方法区(Metaspace)、直接内存(Direct Memory)等区域内存不足时。

本文从复现 OOM监控与捕获Heap DumpMAT 分析四个实战维度,带你一步步排查 OOM。强烈建议结合实际代码运行练习(用 IDEA 或命令行)。

一、OOM 常见类型一览表(先了解敌人)

OOM 类型错误信息示例常见原因发生区域
Java heap spacejava.lang.OutOfMemoryError: Java heap space对象过多、内存泄漏、大数组堆(Heap)
Metaspacejava.lang.OutOfMemoryError: Metaspace类加载过多、动态代理、反射滥用方法区(Metaspace)
Unable to create new native threadjava.lang.OutOfMemoryError: unable to create new native thread线程过多(线程池无上限)系统内存(进程级)
Direct buffer memoryjava.lang.OutOfMemoryError: Direct buffer memory堆外内存滥用(Netty ByteBuf 未 release)直接内存(Off-Heap)
GC overhead limit exceededjava.lang.OutOfMemoryError: GC overhead limit exceededGC 频繁但回收很少(内存泄漏)

结论堆 OOM 最常见(占 80%+),其次是 Metaspace 和直接内存。

二、复现 OOM(实战第一步:本地模拟线上问题)

用简单代码复现各种 OOM,方便理解和测试。运行时加 JVM 参数 -Xmx128m(限制堆最大 128MB)来加速复现。

1. 堆 OOM(Java heap space)

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]);  // 每次加 1MB 数组
        }
    }
}
  • 运行:java -Xmx128m HeapOOM
  • 预期:很快 OOM,日志显示 “Java heap space”

2. Metaspace OOM

用 CGLIB 生成海量类(模拟 Spring AOP 滥用)。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class MetaspaceOOM {
    public static void main(String[] args) {
        int i = 0;
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create();
            System.out.println(++i);
        }
    }
}
  • 运行:java -XX:MaxMetaspaceSize=128m MetaspaceOOM
  • 预期:类加载过多,导致 Metaspace OOM

3. 直接内存 OOM(Direct buffer memory)

import java.nio.ByteBuffer;

public class DirectOOM {
    public static void main(String[] args) {
        while (true) {
            ByteBuffer.allocateDirect(1024 * 1024 * 10);  // 每次 10MB 直接内存
        }
    }
}
  • 运行:java -Xmx128m -XX:MaxDirectMemorySize=128m DirectOOM
  • 预期:直接内存耗尽 OOM

4. 线程 OOM(unable to create new native thread)

public class ThreadOOM {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            System.out.println("线程数:" + i);
        }
    }
}
  • 运行:直接运行,视系统线程上限而定(Linux 默认 ~8000+)
  • 预期:线程过多 OOM

复现小贴士:用小内存参数(如 -Xmx128m)加速;线上别这么玩。

三、监控与捕获 OOM(实战第二步:线上准备)

1. JVM 参数配置(必须加这些)

-XX:+HeapDumpOnOutOfMemoryError      # OOM 时自动 Dump 堆
-XX:HeapDumpPath=/data/dump.hprof    # Dump 文件路径
-XX:+PrintGCDetails                  # 打印 GC 细节
-XX:+PrintGCTimeStamps               # GC 时间戳
-Xloggc:/data/gc.log                 # GC 日志
-XX:MaxMetaspaceSize=256m            # 限制 Metaspace(默认无限)
-XX:MaxDirectMemorySize=128m         # 限制直接内存

2. 监控工具(线上必备)

  • jstat:实时监控 GC、内存使用 jstat -gcutil pid 1000(每秒输出)
  • jmap:查看堆使用 jmap -heap pid
  • VisualVM / JConsole:图形化监控(本地/远程连接)
  • Arthas:线上神器 java -jar arthas-boot.jarheapdump /data/dump.hprof
  • Prometheus + Grafana:线上监控堆使用率、GC 频率

捕获技巧:OOM 前看 GC 日志,如果 Old 区持续上涨且 Full GC 回收很少 → 99% 是内存泄漏。

四、Heap Dump 与分析准备(实战第三步:获取证据)

1. 如何 Dump Heap

  • 自动 Dump:用上面 JVM 参数,OOM 时自动生成
  • 手动 Dump
  • jmap -dump:live,format=b,file=/data/dump.hprof pid(推荐 live,只 Dump 存活对象)
  • jcmd pid GC.heap_dump /data/dump.hprof(JDK 9+ 推荐)
  • Arthas:heapdump /data/dump.hprof --live

注意:Dump 时会暂停 JVM(STW),线上高峰期慎用;Dump 文件很大(堆大小的 1~2 倍),准备足够磁盘。

2. 下载与传输

线上 Dump 后,用 scp / rsync 下载到本地:scp user@host:/data/dump.hprof .

五、MAT Dump 分析(实战第四步:找出元凶)

MAT(Eclipse Memory Analyzer Tool)是最强 Heap Dump 分析工具,下载地址:https://eclipse.dev/mat/(免费)。

1. 打开 Dump 文件

  • 启动 MAT → File → Open Heap Dump → 选择 hprof 文件
  • 等待解析(大文件需几分钟)

2. 核心视图与分析步骤(最实用流程)

  1. Overview:看总体情况(堆大小、类数、对象数、线程数)
  2. Leak Suspects(最重要!):MAT 自动检测可疑泄漏点
  • Problem Suspect 1:通常是最大的对象或集合
  • 看 Description:如 “One instance of java.util.HashMap loaded by … occupies 80MB”
  1. Dominator Tree(支配树):看保留集最大的对象(前 10 名通常是元凶)
  • 展开树:看哪个 Map/List/Set 占内存最大
  • 右键 → Path To GC Roots → Exclude weak/soft references:找谁持有它不放
  1. Histogram:按类统计对象数/大小
  • 过滤 “java.util.*”:看哪些集合类对象最多
  • 右键 → Merge Shortest Paths to GC Roots
  1. Threads:看线程栈,检查 ThreadLocal 或线程持有的对象

3. 经典 OOM 分析案例

  • 案例1:HashMap 泄漏(最常见)
  • MAT 中 Dominator Tree 前排是 HashMap
  • 找 GC Roots:通常是静态 Map 或 ThreadLocalMap
  • 修复:加过期策略 / remove ThreadLocal
  • 案例2:大数组(如缓存图片)
  • Histogram 中 byte[] / char[] 对象很大
  • 找持有者:可能是 List 或缓存类
  • 案例3:Metaspace OOM
  • 用 jcmd / jmap 看类加载数
  • MAT 看 ClassLoader 树

MAT 小技巧

  • OQL 查询:SELECT * FROM java.util.HashMap WHERE size > 1000(SQL-like 查大集合)
  • Compare Baskets:对比两个 Dump(前 vs 后)看增长对象
  • 内存大时用 64 位 MAT + 加堆 -Xmx8g

六、总结 & 预防 OOM 的最佳实践

排查口诀

  • 先看日志:哪个区域 OOM
  • 复现本地:模拟代码
  • Dump Heap:自动/手动
  • MAT 分析:Leak Suspects → Dominator Tree → GC Roots

预防

  1. 设置合理堆大小(-Xmx 至少机器内存 1/2 ~ 3/4)
  2. 用 Caffeine / Guava Cache 代替裸 HashMap
  3. ThreadLocal 用完 remove()
  4. 监控 GC / 内存(Prometheus)
  5. 线上 Dump 前准备好路径和空间

掌握这个流程,90% 的 OOM 都能快速定位。

有具体案例想模拟(比如 Netty OOM)、MAT 安装问题、或某个步骤代码细节?欢迎继续问~

文章已创建 4455

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部