生产环境注意事项:
- 堆转储会触发 Full GC,在高峰期可能导致服务短暂停顿。尽量在低峰期操作。
- 堆转储文件可能很大(等于堆大小,几十 GB)。确保磁盘空间充足。
- 堆转储生成需要时间(大堆可能需要几分钟)。不要在生成一半时停止进程。
- 使用
-XX:+HeapDumpOnOutOfMemoryError 代替手动触发,这样只在 OOM 时才生成,不影响正常服务。
:::
1.2 离线转储与在线诊断
1.3 堆转储的格式
.hprof 文件包含:
Heap Dump
├── 类实例(Instance)
│ ├── 对象头(Object Header)
│ ├── 实例字段(Instance Fields)
│ └── 引用(References)
├── 类信息(Class)
│ ├── 类加载器
│ ├── 静态字段
│ └── 常量池
├── GC Roots
└── 对象引用图(邻接表)
二、MAT 使用指南 🔴
2.1 核心概念
Shallow Heap vs Retained Heap:
示例:
HashMap 实例
→ Shallow Heap: 48 字节(HashMap 自身大小)
→ Retained Heap: 3.2GB(HashMap + 它持有的所有 Entry + 所有 value 对象)
Retained Heap 才是判断内存占用的关键指标。
GC Roots:
GC Roots 是堆外指向堆内对象的引用,是对象是否"存活"的判断依据:
GC Roots 包括:
1. JVM 方法区中的类静态属性引用的对象
2. JVM 方法区中的常量引用的对象
3. 活跃的本地方法栈中 JNI 引用的对象
4. 活跃线程持有的对象
5. JNI 全局引用
6. 用于同步的 Monitor 对象
2.2 MAT 的核心功能
功能一:Leak Suspects Report(泄漏嫌疑人报告)
使用步骤:
1. File → Open Heap Dump
2. 选择 "Leak Suspects Report"
3. MAT 自动分析并生成报告
报告内容:
- 可能的内存泄漏点
- 每个泄漏点占用的大小
- 简化的引用链
功能二:Histogram(直方图)
查看所有类的实例数量和内存占用:
Path To GC Roots → exclude weak/soft/phantom refs
→ 显示对象的完整引用链
→ 找到是什么持有这个对象(GC Root)
Dominator Tree
→ 以对象支配关系展示内存结构
→ 快速找到最大的内存占用者
功能三:OQL(对象查询语言)
类似 SQL 的查询语言:
-- 查询 HashMap 中大于 1MB 的 value
SELECT p.value.toString().length() AS size
FROM java.util.HashMap$Node p
WHERE p.value.toString().length() > 1048576
-- 查询特定类的所有实例
SELECT * FROM com.example.UserProfile
-- 查询字段数量超过 1000 的 ArrayList
SELECT * FROM java.util.ArrayList WHERE capacity > 1000
-- 查询持有大对象的 HashMap
SELECT * FROM java.util.HashMap
WHERE objectHeader.@length > 10000
2.3 MAT 分析实战
场景:定位 HashMap 内存泄漏
Step 1: 打开 Leak Suspects Report
发现 "Problem Suspect 1" 占用了 3.2GB
Step 2: 点击进入详情
发现是 com.xxx.UserProfileService.profileCache
Step 3: 查看引用链
Right-click → Path To GC Roots → exclude weak/soft refs
显示:
profileCache (HashMap)
→ userProfileService (Spring Bean)
→ Spring Container
→ ClassLoader
→ Bootstrap ClassLoader (GC Root)
Step 4: 结论
profileCache 被 Spring 单例 Bean 持有,生命周期 = JVM 生命周期
缓存没有大小限制 → 持续膨胀 → OOM
三、JProfiler 使用指南 🟡
3.1 JProfiler vs MAT
3.2 JProfiler 的内存分析视图
Biggest Objects(最大对象视图)
显示堆中占用最大的对象:
1. com.xxx.UserProfileService.profileCache (3.2GB)
└── HashMap$Node[] (3.2GB)
├── Entry: userId_001 → UserProfile (500KB)
├── Entry: userId_002 → UserProfile (480KB)
└── ...
2. com.xxx.OrderService.orderCache (800MB)
Reference View(引用视图)
显示对象的入引用(谁引用了这个对象)和出引用(这个对象引用了谁)。
Unreachable Objects(不可达对象)
分析对象在 GC 之前的状态,了解对象为什么没有被及时回收。
3.3 JProfiler 分析实战
场景:排查服务内存持续增长
Step 1: Attach 到运行中的进程
Session → Attach to JVM
Step 2: 开启内存记录
Memory Views → Start Recording
Step 3: 执行可疑操作(查询、缓存操作等)
Step 4: 停止记录,分析分配
"Recorded Objects" 视图
→ 按类分组,查看哪些类实例数量增长最快
→ "Allocation Call Tree" 查看分配来源
Step 5: 生成堆转储对比
间隔 5 分钟生成两个 dump
对比两个 dump 中对象数量变化
→ 快速定位增长最快的对象类型
四、引用链分析方法 🟡
4.1 引用链分析的三种模式
模式一:入引用分析(谁持有我?)
适用场景:知道哪个对象占用了大量内存,想知道是什么持有它
MAT 操作:
Histogram → 选择目标类 → Right-click → Path To GC Roots
JProfiler 操作:
Objects → 选择实例 → Show Nearest GC Root
模式二:出引用分析(我持有了什么?)
适用场景:分析一个容器的内存结构
MAT 操作:
Histogram → 选择 HashMap → Right-click → List Objects → with outgoing refs
JProfiler 操作:
Instances → 选择 HashMap → Show Paths to GC Roots
模式三:支配树分析(谁支配了我?)
支配关系:如果从 GC Root 到 A 的每条路径都经过 B,则 B 支配 A
支配树的用途:
- 快速找到内存占用最大的根因
- 不需要分析整个引用链,只需看支配者
4.2 常见 GC Root 类型
4.3 排除弱/软引用
面试常见问题:为什么用 "exclude weak/soft references"?
原因:
- WeakReference 和 SoftReference 本身就是设计为可回收的
- 它们持有的对象不应该被视为内存泄漏的原因
- 排除它们可以让引用链更清晰,找到真正的根因
使用场景:
HashMap<String, WeakReference<User>> cache
→ HashMap 本身可能不大
→ 但 cache 引用的对象可能在等待 GC
→ 排除 weak refs 后可以看到真正的内存占用
五、生产排查案例 🟡
// 问题代码
List<String> hugeList = new ArrayList<>();
for (Order order : orders) {
// intern() 会将 String 放入字符串常量池(JDK 7+ 在堆中)
// 但这里过度使用,导致常量池膨胀
hugeList.add(order.getName().intern());
}
排查过程:
# 生成 dump 后用 MAT 分析
# 1. Histogram 视图
# 2. 选择 java.lang.String
# 3. 按 Retained Heap 排序
# 4. 发现有 100 万个 unique String,总共 500MB
# 5. Path To GC Roots → exclude all refs
# 6. 发现这些 String 都在字符串常量池中
# 7. 回到代码,找到 intern() 的使用位置
5.2 案例:ThreadLocal 泄漏
// 问题代码
public class UserContextFilter {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
// ❌ 没有 remove()!线程池复用时会导致内存泄漏
}
排查过程:
# MAT 分析
# 1. 搜索 java.lang.ThreadLocal
# 2. 查看 Retained Heap
# 3. Path To GC Roots
# 4. 发现大量 ThreadLocalMap.Entry 的 key 为 null
# 但 value 指向 User 对象
# 根因:
# ThreadLocalMap.Entry 的 key(ThreadLocal)被回收了
# 但 value(User)仍然被 Entry 持有
# → 内存泄漏
5.3 案例:连接池泄漏
// 问题代码
public Connection getConnection() {
Connection conn = dataSource.getConnection();
// ❌ 异常时没有关闭连接
if (Math.random() > 0.9) {
throw new RuntimeException("random error");
}
return conn;
}
排查过程:
# 1. Histogram 视图
# 2. 查看 java.sql.Connection 子类(如 DruidConnectionImpl)
# 3. 实例数量异常高
# 4. Path To GC Roots
# 5. 发现连接被某个方法局部变量持有
# 6. 确认是异常路径下没有释放连接
六、工具配置参考 🟢
6.1 MAT 配置
# MAT 需要足够的内存来分析大堆转储
# 修改 MemoryAnalyzer.ini(位于 MAT 安装目录)
-vmargs
-Xmx8g # 至少 8GB,用于分析大堆
-Xms4g
-XX:+UseG1GC
6.2 Arthas heapdump 命令
# 使用 Arthas 生成 heapdump(不需要手动下载 MAT)
arthas> heapdump /tmp/heapdump.hprof
arthas> heapdump /tmp/heapdump.hprof -g
# 参数说明:
# /tmp/heapdump.hprof # 指定输出路径
# -g # 包含对象类型信息(默认包含)
# --live # 只 dump 存活对象(GC 前执行)
# 注意:--live 会先触发 Full GC
6.3 dump 文件大小参考
:::tip 💡
面试加分点:能说出"在 16GB 以上的大堆场景下,dump 文件会非常大(可能超过 50GB),MAT 可能无法直接加载。解决方案是使用 IBM Heap Analyzer 或者通过 jhat 进行在线分析",说明他有过大堆转储的实际排查经验。