组件跑着跑着自己挂掉,查看日志报java.lang.OutOfMemoryError: Java heap space,看起来是内存溢出了,具体原因不明,因此准备获取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是用来分析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做查询操作。对可能有大数据量的表的查询需要谨慎处理。
打开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);
EntityManager直接@Autowired就可以被注入。如果确实查询数据量超过了内存限制,就只能在查询方法中分段查询,查完一部分就clear一下,再查下一部分。
@Autowired
private EntityManager entityManager;
@Transactional
public void clearSession() throws BaseException {
entityManager.unwrap(Session.class).clear();
}
这次的问题出现在数据库有八十万数据的情况下,其实在大量数据的时候,使用hibernate确实是要谨慎。之前hibernate的queryPlanCache也引起过内存溢出问题,这次又是缓存问题。另外就是设计的时候需要考虑到数据库会不会有冗余数据,如果有的话要想办法及时删掉。
queryPlanCache内存溢出问题的解决