一、问题描述
业务场景是从第三方系统每天同步两次13万数据到我们的数据库。一开始使用jpa的saveAll() 进行保存,并没有内存问题。但是保存的时间在15分钟左右。于是做了部分改进使用entityManager.createNativeQuery(sql)的方式进行批量写库。改造后每次定时任务大概在2分钟跑完。效率改进还挺满意的。大致伪代码如下:
public int sqlSaveBusinessList(List<TableDTO> list) {
StringBuilder sb = new StringBuilder();
sb.append("insert INTO t_supplier_report_summary (id, a, b, c") VALUES ");
for (int i = 0; i < list.size(); i++) {
TableDTO entity = list.get(i);
sb.append("...")
}
Query insert = entityManager.createNativeQuery(sb.toString());
return insert.executeUpdate();
}
上线两天后,开始收到线上告警, ERROR o.s.integration.handler.LoggingHandler - java.lang.OutOfMemoryError: Java heap space
二、问题分析
使用JvisualVM工具对进程进行监控。发现堆内存阶梯增加。虽然有gc但是有一部分并未释放。
Dump一下
再Dump一下
又多了一条insert语句的实例。第二dump之前我有意触发了一次手工的gc。结果实例还在。证明这个是导致内存溢出的主要原因。对象大小4m还不能给回收。
看了一下引用,发现被QueryPlanCache持有
每次entityManager.createNativeQuery(sql) 会把sql的执行计划缓存起来。但是由于是sql拼接了参数的所以每次sql执行都是不一样。所以执行计划也不一样。导致每次缓存一个。
且只有在 SessionFactory 关闭的时候才会clear缓存。
三、解决方法
1、用占位符代替字符串参数拼接。
2、换jdbc saveAll() 性能较慢
3、用jdbcTemplate代替entityManager