前言

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

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

知识储备

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

1. MAT工具的下载和安装

1.1 下载

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

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_JVM

1.2 安装

解压即可,获得以下目录

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_一级缓存_02

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

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_JVM_03

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_kylin_04

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_JVM_05

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

3.2 内存分析

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_一级缓存_06

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

3.2.1 初步分析

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_一级缓存_07

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_一级缓存_08

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_kylin_09

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_kylin_10

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_一级缓存_11

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_JVM_12

  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 Class,com.nes.kylin.powerstation.domain.clickhouse.bo.DeviceBranchHistoryData 首当其冲,基本可以确定是它的问题了,那如何验证我们的猜想呢?

3.2.2 深入分析

回到预览面板,点击 Histogram,查看类维度的分析

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_kylin_13

 

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_JVM_14

这里我们又看到了 com.nes.kylin.powerstation.domain.clickhouse.bo.DeviceBranchHistoryData

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_kylin_15

单击 with incoming references 查看该类的实例

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_JVM_16

这里得到的是内存中该类的实例列表,我们随意找一个对象,继续单击 with incoming references 查看该对象被谁引用了

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_一级缓存_17

得到如图,我们将对象信息展开,再一次见到了 org.apache.ibatis.cache.impl.PerpetualCache,这个类mybatis的一级缓存类,接下来我们review代码,去分析和解决问题,初步结论是mybatis中的缓存没有及时释放。

[转帖]JVM OOM内存溢出分析实战(基于MAT工具)_kylin_18

再结合程序运行时趋势递增的内存变化曲线,基本可以确定有大量的对象没能及时释放内存,造成了堆积,最后发生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 <= #{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内存溢出问题。在实际应用中,会有更加复杂多变的情况,要结合实际的情况去分析。