https://juejin.cn/post/7342679747636215858?searchId=202404070705500138968B5F69BF68EA87
前言
在Java开发与运维过程中,JVM内存管理扮演着至关重要的角色,尤其在面临高性能、大数据量处理的场景时,如何有效防止和解决JVM堆OOM问题显得尤为关键。
最近写了一个涉及海量数据计算的功能,在线上发生了OOM,本文介绍了基于MAT工具分析堆内存溢出的过程。
知识储备
阅读本文需要对JVM内存模型和GC有一定的知识储备,对Spring事务管理和Mybatis有一定的了解。
1. MAT工具的下载和安装
1.1 下载
选择自己需要的版本,我本次使用的是Windows版本:官网下载地址
1.2 安装
解压即可,获得以下目录
MemoryAnalyzer.exe 为启动文件,双击启动即可
2. JVM内存快照—dump文件的生成
线上的做法是添加JVM参数,以便在发生OOM时生成内存快照。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\AppData\Mat\kylin2.hprof
也可以使用jmap手动dump
jmap -dump:live,format=b,file=<filename.hprof> <pid>
3. 实战分析
3.1 快照导入MAT
选择 Open Heap Dump,打开快照文件,会得到一个预览面板
3.2 内存分析
点击详情后其实我们可以获得很多直观而且关键的信息了
3.2.1 初步分析
- 黄色区域大概意思是主线程占用了99.17%的内存,内存集中在一个HashMap中并占用了96.47%;
- 查看 Shortest Paths To the Accumulation Point ,我们发现了
java.util.HashMap
是被一个org.apache.ibatis.cache.impl.PerpetualCache
引用了,这个类是嘛呢,有经验的同学其实可以推断出来了,是与mybatis缓存相关的类,继续往下看;- 查看 Accumulated Objects in Dominator Tree ,这是一个引用链,也可以很直观的看到主线程中有一个
org.apache.ibatis.cache.impl.PerpetualCache
引用了大量的java.util.HashMap
,PerpetualCache
的内部实现就是HashMap
,只是现在我们还不知道HashMap中存了什么,先继续往下看;- 我们直接看 All Accumulated Objects by Class,
com.nes.kylin.powerstation.domain.clickhouse.bo.DeviceBranchHistoryData
首当其冲,基本可以确定是它的问题了,那如何验证我们的猜想呢?
3.2.2 深入分析
回到预览面板,点击 Histogram,查看类维度的分析
这里我们又看到了
com.nes.kylin.powerstation.domain.clickhouse.bo.DeviceBranchHistoryData
单击 with incoming references 查看该类的实例
这里得到的是内存中该类的实例列表,我们随意找一个对象,继续单击 with incoming references 查看该对象被谁引用了
得到如图,我们将对象信息展开,再一次见到了
org.apache.ibatis.cache.impl.PerpetualCache
,这个类mybatis的一级缓存类,接下来我们review代码,去分析和解决问题,初步结论是mybatis中的缓存没有及时释放。
再结合程序运行时趋势递增的内存变化曲线,基本可以确定有大量的对象没能及时释放内存,造成了堆积,最后发生OOM。
4. 结论
4.1 代码分析
for (String targetStationCode : targetStationCodes) {
shadowRecognitionBizService.forecastByStation(targetStationCode, dataTime, stationCleanInfoMap.get(targetStationCode), randomForestInstancesPair, deleteTag, algoPartitionDays);
...
}
@Transactional(rollbackFor = Exception.class)
public void forecastByStation(String stationCode, Date dataTime, Date cleanDate,
Pair<RandomForest, Instances> randomForestInstancesPair,
int deleteTag, int algoPartitionDays) throws ParseException {
...
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nes.kylin.powerstation.domain.clickhouse.dao.ClickhouseDeviceMapper">
...
<select id="selectBranchData" resultType="com.nes.kylin.powerstation.domain.clickhouse.bo.DeviceBranchHistoryData">
SELECT
toUnixTimestamp(ts,'Asia/Shanghai') AS dateTs,
ts AS date,
sn AS sn,
${pointBranch}
FROM ${tableName}
where qos = 1
and sn in
<foreach collection="sns" item="item" open="(" separator="," close=")">
#{item}
</foreach>
and ts >= #{startTime} and ts <= #{endTime}
order by ts
</select>
...
</mapper>
这里循环
targetStationCodes
,在shadowRecognitionBizService.forecastByStation()
方法中声明了事务,并且有通过 mybatis 分批次查询较大数量级数据的操作。我们基本可以断定是selectBranchData
这个查询返回的DeviceBranchHistoryData
在内存中占用的空间没能释放,造成了堆积。
4.2 结论
结合之前的分析,DeviceBranchHistoryData
的实例是被PerpetualCache
引用的,那问题的根源就是PerpetualCache
了。
熟悉mybatis的同学知道,PerpetualCache 是mybatis一级缓存的类(这里不对mybatis做展开分析)。mybatis一级缓存的生命周期是依赖于SqlSession,SqlSession关闭了,一级缓存也会被清空。那为什么没能及时释放呢?因为在Spring环境中,一个 @Transactional
下使用的SqlSession是同一个,即事务没结束,SqlSession没close,缓存没能释放,造成了对象堆积,引发了OOM。
在我的场景中,我选择保证原有代码逻辑,关闭这个sql的一级缓存,即在select标签中添加 flushCache="true"
。
5. 总结
综上,本文通过实际案例结合MAT工具,分析和解决了突发的JVM内存溢出问题。在实际应用中,会有更加复杂多变的情况,要结合实际的情况去分析。