Skip to content

# 内存泄漏诊断

1. 并发请求诊断-JMeter

并发请求问题指的是用户通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。

1.1 使用JMeter进行并发测试,发现内存溢出问题

背景:小李的团队发现有一个微服务在晚上8点左右用户使用的高峰期会出现内存溢出的问题,于是他们希望在自己的开发环境能重现类似的问题。
步骤:

  1. 安装JMeter软件,添加线程组。
  2. 在线程组中增加Http请求,添加随机参数。
  3. 在线程组中添加监听器–聚合报告,用来展示最终结果。
  4. 启动程序,运行线程组并观察程序是否出现内存溢出。

1.2 编写接口代码:

java
@RestController()
@RequestMapping("/leak")
public class UserController {

    static class UserEntity{
        Long id;
        String name;

        public UserEntity(Long id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    private static Map<Long,UserEntity> userCache = new HashMap<>();
    private static List<byte[]> bigheap = new ArrayList<>();

    @GetMapping("/test")
    public void test1() throws InterruptedException {
        byte[] bytes = new byte[1024 * 1024 * 100];//100m
        Thread.sleep(10 * 1000L);
    }

    @PostMapping("/login")
    public void login(String name,Long id){
        userCache.put(id,new UserEntity(id,name));
    }
}

运行接口代码,加上JVM参数:-Xmx1g -Xms1g,启动程序。

1.3 诊断GET接口

启动JMeter后, 分别配置线程组和请求 Alt text 线程组大小为100:
ScreenShot_2026-04-16_225218_700.png 选择线程组后,右键选择取样器: ScreenShot_2026-04-16_224723_801.png 配置HTTP请求: ScreenShot_2026-04-16_225442_762.png 继续选择线程组后,右键选择监听器-聚合报告: ScreenShot_2026-04-16_225541_043.png 点击开始即可在聚合报告中查看当前请求的结果: ScreenShot_2026-04-16_225720_229.png

1.4 诊断POST接口

添加线程组2: ScreenShot_2026-04-16_230516_471.png 在线程组2中添加HTTP请求: ScreenShot_2026-04-16_231113_846.png 使用JMeter的工具助手生成value表达式: ScreenShot_2026-04-16_231250_320.png 分别生成id和name的表达式: ScreenShot_2026-04-16_231437_002.png 填充HTTP请求的参数: ScreenShot_2026-04-16_231555_861.png 继续选择线程组后,右键选择监听器-聚合报告: ScreenShot_2026-04-16_231746_758.png 这时选择线程组2,右键点击开始: ScreenShot_2026-04-16_231922_286.png 可以在聚合报告中查看线程组2的请求结果: ScreenShot_2026-04-17_095522_627.png

JMeter官网推荐使用命令行

JMeter推荐通过GUI界面创建测试计划,然后使用命令行运行测试计划。比如上面的测试计划,可以使用以下命令行运行:

cmd
D:\apache-jmeter-5.6.3\bin>jmeter.bat -n -t ../test/jvm1.jmx -l ../logs/log.jtl
WARN StatusConsoleListener The use of package scanning to locate plugins is deprecated and will be removed in a future release
WARN StatusConsoleListener The use of package scanning to locate plugins is deprecated and will be removed in a future release
WARN StatusConsoleListener The use of package scanning to locate plugins is deprecated and will be removed in a future release
WARN StatusConsoleListener The use of package scanning to locate plugins is deprecated and will be removed in a future release
Creating summariser <summary>
Created the tree successfully using ../test/jvm1.jmx
Starting standalone test @ 2026 Apr 17 09:48:15 CST (1776390495662)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary +  40079 in 00:01:16 =  528.6/s Avg:    29 Min:     0 Max: 50977 Err: 39000 (97.31%) Active: 20 Started: 20 Finished: 0
summary +     23 in 00:00:30 =    0.8/s Avg:  9626 Min:    66 Max: 10162 Err:    23 (100.00%) Active: 20 Started: 20 Finished: 0
summary =  40102 in 00:01:46 =  378.4/s Avg:    34 Min:     0 Max: 50977 Err: 39023 (97.31%)

需要注意的是JMeter的命令行运行会将测试计划下所有的线程组都运行,因此如果只需要跑其中的某个线程组,可以使用JMeter的GUI界面运行,并选择其他线程组将其禁用。我们可以关闭线程组2的运行。

2. 定位内存泄露-内存快照

当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile)文件。生成内存快照的Java虚拟机参数:

  • -XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
  • -XX:HeapDumpPath=<path>:指定hprof文件的输出路径,不指定的话默认生成到当前目录,名称格式为java_pid<pid>.hprof。

使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。比如测试的SpringBoot项目产生的文件如下: ScreenShot_2026-04-17_105431_686.png 虽然我们设置的最大堆内存是1G,但是我们的hprof内存快照文件不足1G(文件大小为924M),这是由于运行过程中,调用的接口/leak/test接口会增加100M内存占用,但当前内存占用是924M如果再加上100M就内存溢出报错,这部分内存就没有内存快照。 ScreenShot_2026-04-17_110625_117.png 点击details,可以查看调用栈的详情,帮助我们定位内存溢出的位置: ScreenShot_2026-04-17_111013_963.png

3. MAT工具内存泄露检测原理

MAT提供了称为支配树(Dominator Tree)的对象图。支配树展示的是对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。 345363452342353dfgdf.png

3.1 深堆和浅堆

支配树中对象本身占用的空间称之为浅堆(Shallow Heap)。支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的深堆(Retained Heap),也称之为保留集(Retained Set)。深堆的大小表示该对象如果可以被回收,能释放多大的内存空间。 ScreenShot_2026-04-20_233132_423.png 在不内存溢出情况下生成堆内存快照?-XX:+HeapDumpBeforeFullGC可以在FullGC之前就生成内存快照。 ScreenShot_2026-04-20_234008_264.png 引用链转换成支配树: ScreenShot_2026-04-20_234556_193.png MAT就是根据支配树,从叶子节点向根节点遍历,如果发现深堆的大小超过整个堆内存的一定比例阈值,就会将其标记成内存泄漏的“嫌疑对象”。

4. 导出运行中系统的内存快照并进行分析

导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:

  1. 通过JDK自带的jmap命令导出,格式为:jmap -dump:live,format=b,file=文件路径和文件名 进程ID
  2. 通过arthas的heapdump命令导出,格式为:heapdump --live 文件路径和文件名

5. 分析超大堆的内存快照

在程序员开发用的机器内存范围之内的快照文件,直接使用MAT打开分析即可。但是经常会遇到服务器上的程序占用的内存达到10G以上,一般下载到开发机比较耗时,其次开发机内存一般不是很大,无法正常打开此类内存快照。此时需要下载服务器操作系统对应的 MAT。下载地址:https://eclipse.dev/mat/downloads.php (最新版的MAT只支持JDK11以上)通过MAT中的脚本生成分析报告:

shell
./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components

警告

默认MAT分析时只使用了1G的堆内存,如果快照文件超过1G,需要修改MAT目录下的MemoryAnalyzer.ini配置文件调整最大堆内存。