# 内存泄漏诊断
1. 并发请求诊断-JMeter
并发请求问题指的是用户通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。
1.1 使用JMeter进行并发测试,发现内存溢出问题
背景:小李的团队发现有一个微服务在晚上8点左右用户使用的高峰期会出现内存溢出的问题,于是他们希望在自己的开发环境能重现类似的问题。
步骤:
- 安装JMeter软件,添加线程组。
- 在线程组中增加Http请求,添加随机参数。
- 在线程组中添加监听器–聚合报告,用来展示最终结果。
- 启动程序,运行线程组并观察程序是否出现内存溢出。
1.2 编写接口代码:
@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后, 分别配置线程组和请求
线程组大小为100:
选择线程组后,右键选择取样器:
配置HTTP请求:
继续选择线程组后,右键选择监听器-聚合报告:
点击开始即可在聚合报告中查看当前请求的结果: 
1.4 诊断POST接口
添加线程组2:
在线程组2中添加HTTP请求:
使用JMeter的工具助手生成value表达式:
分别生成id和name的表达式:
填充HTTP请求的参数:
继续选择线程组后,右键选择监听器-聚合报告:
这时选择线程组2,右键点击开始:
可以在聚合报告中查看线程组2的请求结果: 
JMeter官网推荐使用命令行
JMeter推荐通过GUI界面创建测试计划,然后使用命令行运行测试计划。比如上面的测试计划,可以使用以下命令行运行:
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项目产生的文件如下:
虽然我们设置的最大堆内存是1G,但是我们的hprof内存快照文件不足1G(文件大小为924M),这是由于运行过程中,调用的接口/leak/test接口会增加100M内存占用,但当前内存占用是924M如果再加上100M就内存溢出报错,这部分内存就没有内存快照。
点击details,可以查看调用栈的详情,帮助我们定位内存溢出的位置: 
3. MAT工具内存泄露检测原理
MAT提供了称为支配树(Dominator Tree)的对象图。支配树展示的是对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。 
3.1 深堆和浅堆
支配树中对象本身占用的空间称之为浅堆(Shallow Heap)。支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的深堆(Retained Heap),也称之为保留集(Retained Set)。深堆的大小表示该对象如果可以被回收,能释放多大的内存空间。
在不内存溢出情况下生成堆内存快照?-XX:+HeapDumpBeforeFullGC可以在FullGC之前就生成内存快照。
引用链转换成支配树:
MAT就是根据支配树,从叶子节点向根节点遍历,如果发现深堆的大小超过整个堆内存的一定比例阈值,就会将其标记成内存泄漏的“嫌疑对象”。
4. 导出运行中系统的内存快照并进行分析
导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:
- 通过JDK自带的jmap命令导出,格式为:
jmap -dump:live,format=b,file=文件路径和文件名 进程ID - 通过arthas的heapdump命令导出,格式为:
heapdump --live 文件路径和文件名
5. 分析超大堆的内存快照
在程序员开发用的机器内存范围之内的快照文件,直接使用MAT打开分析即可。但是经常会遇到服务器上的程序占用的内存达到10G以上,一般下载到开发机比较耗时,其次开发机内存一般不是很大,无法正常打开此类内存快照。此时需要下载服务器操作系统对应的 MAT。下载地址:https://eclipse.dev/mat/downloads.php (最新版的MAT只支持JDK11以上)通过MAT中的脚本生成分析报告:
./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components警告
默认MAT分析时只使用了1G的堆内存,如果快照文件超过1G,需要修改MAT目录下的MemoryAnalyzer.ini配置文件调整最大堆内存。
