• Java堆内存溢出问题分析和解决


    起因

    组件跑着跑着自己挂掉,查看日志报java.lang.OutOfMemoryError: Java heap space,看起来是内存溢出了,具体原因不明,因此准备获取dump文件拿来分析下。

    获取dump文件

    JVM环境变量设置:
    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${_HOME_DIR}/logs/xxx.hprof
    加入该参数后,内存溢出后就会在规定目录自动生成.hprof文件了,如果是测试环境,为了快速复现,可以把内存设置改小一点,比如-Xmx512m -Xms512m -xmn128m.

    另:
    使用jmap -dump:format=b,file=xxx.hprof [pid] 也是可以导出dump文件的,但是这次我并没有用到,就不赘述了。

    下载MAT工具

    MAT是用来分析dump文件的,下载地址
    注意选择自己电脑对应的版本就可以了

    分析dump文件

    解压MAT之后点击MemoryAnalyzer.exe,即可启动
    左上角File-open heap dump,打开之后弹窗选择Leak suspects report(默认)
    它就会有这样的一个内存占用情况:
    内存占用情况图
    显然,确实是有一个东西,把内存都占完了,但是是什么呢…看着好像是hibernate什么什么QueryParameterBindingsImpl.expandListValueParameters,但具体好像看不出什么来,此时可以打开dominator_tree
    在这里插入图片描述
    找到占用最多的部分展开,其实可以看到是StatefulPersistenceContext。随便找一个String,List objects with incoming references,也可以看到指向的还是PersistenceContext。在这里插入图片描述
    在这里插入图片描述

    猜想和解决方式

    PersistenceContext是hibernate的一级缓存,且hibernate的一级缓存是无法被关闭的。并且在查询时,hibernate发现如果缓存中没有,就会把数据在缓存中存一份,所以确实是很占缓存的…
    但是一般来说hibernate的一级缓存是会随着session的结束而回收的(或者说,事务的结束而回收),所以除非在某个session中进行了大量的数据查询,一般来说问题不大。XNIO-2这个线程名称,看着就是对外提供的某个接口,猜测是因为某些查询条件(提供给别的组件调用的接口)偶发的大量数据查询导致。
    此时将gc日志加入分析,发现定时在每天一点时,就会出现非常频繁的full gc,过了这个点之后内存明显下降,且不再触发full gc。此时去查调用方的代码发现,每天一点有一个定时同步的逻辑,基本可以确认是同步逻辑导致的,考虑进行代码的优化。

    避免不需要的查询

    首先是在代码中出现了"通过findByKeyIn查询表A数据,再通过查询到的ID去删除表B的数据(一个key可能对应几十万ID,更何况是keylist,这个数据量肯定是非常大的)",解决方式是既然key和表B的ID有对应关系,写入时就将key写入表B,删除时直接通过key去删除表B的数据,这样就无需对表A做查询操作。对可能有大数据量的表的查询需要谨慎处理。

    注意delete语句

    打开spring.jpa.show-sql后,观察sql发现使用JPA自带的deleteByXxxIn语句时,会先查询符合条件的记录(进入一级缓存)再一条一条删除。因此在大批量做删除操作时(前文提到,通过KeyList删除,而一个Key可能对应非常多个ID),不要使用JPA自带的deleteByXxxIn,而是使用@Query.

        @Modifying
        @Query("delete from table_name s where s.key in :keyList ")
        int deleteByKeyInBatch(@Param("keyList") List<String> keyList);
    
    • 1
    • 2
    • 3

    entityManager清空缓存

    EntityManager直接@Autowired就可以被注入。如果确实查询数据量超过了内存限制,就只能在查询方法中分段查询,查完一部分就clear一下,再查下一部分。

        @Autowired
        private EntityManager entityManager;
    
        @Transactional
        public void clearSession() throws BaseException {
            entityManager.unwrap(Session.class).clear();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    后记

    这次的问题出现在数据库有八十万数据的情况下,其实在大量数据的时候,使用hibernate确实是要谨慎。之前hibernate的queryPlanCache也引起过内存溢出问题,这次又是缓存问题。另外就是设计的时候需要考虑到数据库会不会有冗余数据,如果有的话要想办法及时删掉。
    queryPlanCache内存溢出问题的解决

  • 相关阅读:
    【Web系列二十五】前后端使用proto+grpc实现数据传输
    SpringMVC系列-4 参数解析器
    接口优化例子
    Java学习之方法重写/覆盖
    RabbitMQ(五)【入门案例】
    上周热点回顾(4.11-4.17)
    y7000p 2020h风扇狂转解决办法
    rust学习-any中的downcast和downcast_ref
    网络-电脑网络突然变成球形, 网络不可用
    唐山盐碱滩成渤海明珠 国稻种芯·中国水稻节:河北曹妃甸大米
  • 原文地址:https://blog.csdn.net/yogima/article/details/127854183