MAT(Memory Analyzer Tool)深度分析实录
MAT(Memory Analyzer Tool)深度分析实录
将几个 G 大小的 .hprof 快照文件下载并导入 Eclipse MAT 工具进行排查。
[GC Roots (静态变量 / 活动线程)]
│ (Strong Reference 强引用链路未切断)
▼
[com.example.billing.service.ReportExportService]
│
▼
[java.util.concurrent.ConcurrentHashMap]
│ (持有 2,500,000+ 个 Entry)
▼
[com.example.billing.dto.FinancialRecordDTO] <-- 吞噬 3.2GB 内存
- 查看 Leak Suspects 报告
MAT 打开后首先分析内存泄漏疑似点。报告首行给出明确警告:
“The class java.util.concurrent.ConcurrentHashMap, loaded by , occupies 3,421,123,072 bytes (78.34%) of memory. The memory is accumulated in one instance of com.example.billing.service.ReportExportService.”
- 借助 Dominator Tree(支配树)定位大对象
展开 Dominator Tree,按 Retained Heap(保留内存大小)降序排列。
发现单一集合实例占用近 3.4GB 内存。集合内部塞满了 250 万个 FinancialRecordDTO 对象,每个对象包含上百个明细字段。
- 检查 Path to GC Roots(排查无法释放的原因)
右键该对象 -> Path To GC Roots -> 选择 exclude all phantom/weak/soft references(排除虚/弱/软引用)。
追踪出的引用链清晰显示:该集合被一个声明为 @Service 的 Spring 单例 Bean 内部的 private final Map<String, List> cacheMap 强引用所持有。
四、代码根因分析与代码重构
【问题代码还原】
研发团队为了加速“月末多维度财务报表导出”的性能,编写了类似如下逻辑:
Java
@Service
public class ReportExportService {
// 危险:常驻单例内存的静态/属性级缓存,且无淘汰机制
private final Map<String, List> exportTaskCache = new ConcurrentHashMap<>();
public void processExport(String taskId, List<Long> recordIds) {
// 每次导出拉取全量明细并无脑堆积在内存 Map 中
List<FinancialRecordDTO> records = financialRecordMapper.batchSelect(recordIds);
exportTaskCache.put(taskId, records);
// 异步线程处理导出文件...
// 关键缺陷:任务异常终止或导出成功后,未在 finally 块中调用 exportTaskCache.remove(taskId)!
}
}
【重构与优化方案】
杜绝使用无界 JVM 本地集合做缓存:若必须使用单机内存缓存,使用带 最大容量限制 和 TTL 自动驱逐策略 的 Caffeine 替代原生 Map。
大数据量流式导出:避免将百万级大集合一次性 Load 进内存,改用 MyBatis 的 ResultHandler 配合 EasyExcel 进行流式写出。
Java
// 正确做法:使用带上限与淘汰策略的 Caffeine Cache 兜底
private final Cache<String, List> safeTaskCache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
// 大数据量流式导出(降低 JVM 峰值压力)
public void streamExport(HttpServletResponse response) throws IOException {
response.setContentType(“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”);
// 使用流式写出,内存中常驻仅数十条记录
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), FinancialRecordDTO.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("财务明细").build();
// 流式遍历游标查询,逐批次写入磁盘/网络 IO,避免一次性消耗堆内存
financialRecordMapper.selectWithStream(resultContext -> {
FinancialRecordDTO record = resultContext.getResultObject();
excelWriter.write(Collections.singletonList(record), writeSheet);
});
excelWriter.finish();
}
五、生产环境高频 OOM 避坑清单
ThreadLocal 忘记 remove():在线程池环境下,核心线程复用导致线程私有变量生命周期无限延长,其持有的上下文对象永远无法被 GC。
滥用堆外内存(DirectBuffer / Netty):未限制堆外内存大小(-XX:MaxDirectMemorySize),Native 内存耗尽触发操作系统直接杀死进程,且堆快照中看不到异常。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)