JVM OOM内存溢出分析实战(基于MAT工具)

前言

在Java开发与运维过程中,JVM内存管理扮演着至关重要的角色,尤其在面临高性能、大数据量处理的场景时,如何有效防止和解决JVM堆OOM问题显得尤为关键。

最近写了一个涉及海量数据计算的功能,在线上发生了OOM,本文介绍了基于MAT工具分析堆内存溢出的过程。

知识储备

阅读本文需要对JVM内存模型和GC有一定的知识储备,对Spring事务管理和Mybatis有一定的了解。

1. MAT工具的下载和安装

1.1 下载

选择自己需要的版本,我本次使用的是Windows版本:官网下载地址

1.2 安装

解压即可,获得以下目录

MemoryAnalyzer.exe 为启动文件,双击启动即可

2. JVM内存快照---dump文件的生成

线上的做法是添加JVM参数,以便在发生OOM时生成内存快照。

shell 复制代码
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\AppData\Mat\kylin2.hprof

也可以使用jmap手动dump

shell 复制代码
jmap -dump:live,format=b,file=<filename.hprof> <pid>

3. 实战分析

3.1 快照导入MAT

选择 Open Heap Dump,打开快照文件,会得到一个预览面板

3.2 内存分析

点击详情后其实我们可以获得很多直观而且关键的信息了

3.2.1 初步分析

  1. 黄色区域大概意思是主线程占用了99.17%的内存,内存集中在一个HashMap中并占用了96.47%;
  2. 查看 Shortest Paths To the Accumulation Point ,我们发现了java.util.HashMap是被一个 org.apache.ibatis.cache.impl.PerpetualCache引用了,这个类是嘛呢,有经验的同学其实可以推断出来了,是与mybatis缓存相关的类,继续往下看;
  3. 查看 Accumulated Objects in Dominator Tree ,这是一个引用链,也可以很直观的看到主线程中有一个 org.apache.ibatis.cache.impl.PerpetualCache 引用了大量的java.util.HashMapPerpetualCache的内部实现就是HashMap,只是现在我们还不知道HashMap中存了什么,先继续往下看;
  4. 我们直接看 All Accumulated Objects by Classcom.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 代码分析

java 复制代码
for (String targetStationCode : targetStationCodes) {
    shadowRecognitionBizService.forecastByStation(targetStationCode, dataTime, stationCleanInfoMap.get(targetStationCode), randomForestInstancesPair, deleteTag, algoPartitionDays);
    
    ...
    
}
java 复制代码
@Transactional(rollbackFor = Exception.class)
public void forecastByStation(String stationCode, Date dataTime, Date cleanDate,
                              Pair<RandomForest, Instances> randomForestInstancesPair,
                              int deleteTag, int algoPartitionDays) throws ParseException {

    ...

}
xml 复制代码
<?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 &lt;= #{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内存溢出问题。在实际应用中,会有更加复杂多变的情况,要结合实际的情况去分析。

相关推荐
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp4 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶4 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb