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内存溢出问题。在实际应用中,会有更加复杂多变的情况,要结合实际的情况去分析。

相关推荐
李慕婉学姐9 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆10 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin11 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model200511 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉11 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国11 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_9418824812 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈12 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_9912 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹12 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理