将30GB大CSV导入H2数据库时遭遇Java堆内存溢出问题
超大CSV导入H2堆内存溢出问题排查与解决
核心原因
- Hibernate一级缓存堆积:
saveAll执行后,Hibernate的Session(一级缓存)会默认保留所有持久化实体对象,不会自动清理,累积15次100万条后,缓存中的实体占用大量堆内存。 - 字符串与对象引用未释放:CSV解析生成的字符串(
split数组、实体属性)被缓存中的实体持有,导致这些对象无法被GC回收,进一步加剧内存占用。 - 批量插入配置缺失:默认Spring Data JPA未开启真正的批量插入,Hibernate可能单条执行SQL,且未及时刷新会话,导致缓存无法释放。
- ArrayList扩容内存浪费:每次创建空
ArrayList时默认容量为10,添加到100万条会多次扩容,扩容产生的旧数组可能暂时无法被回收。
修复方案
1. 强制清理Hibernate会话缓存
注入EntityManager,在每次批量插入后手动刷新并清空会话,释放实体对象:
import javax.persistence.EntityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class RecordService{ private final RecordDao recordDao; private final EntityManager entityManager; @Autowired public RecordService(RecordDao recordDao, EntityManager entityManager) { this.recordDao = recordDao; this.entityManager = entityManager; } @Transactional // 确保事务边界正确 public void readAndWriteToH2(){ String path = "path/to/csv-file.csv"; List<RecordEntity> records = new ArrayList<>(1_000_000); // 预先指定容量 try (BufferedReader br = new BufferedReader(new FileReader(path))) { String line = br.readLine(); // 跳过表头 int lineCount = 0; while ((line = br.readLine()) != null) { String[] split = line.split(','); RecordEntity record = new RecordEntity(); record.prop0 = split[0]; record.prop1 = split[1]; record.prop2 = split[2]; records.add(record); lineCount++; if(lineCount >= 1_000_000){ recordDao.saveAll(records); entityManager.flush(); // 立即将缓存数据写入数据库 entityManager.clear(); // 清空会话缓存,释放实体 records.clear(); // 清空列表,协助GC回收 lineCount = 0; } } // 处理剩余不足100万的记录 if(!records.isEmpty()){ recordDao.saveAll(records); entityManager.flush(); entityManager.clear(); } } catch (Exception e) { e.printStackTrace(); } } }
2. 配置Hibernate批量插入参数
在application.properties中添加以下配置,开启真正的批量插入,减少数据库交互并优化缓存:
# 设置批量插入大小 spring.jpa.properties.hibernate.jdbc.batch_size=10000 # 对插入语句排序,提升批量执行效率 spring.jpa.properties.hibernate.order_inserts=true # 禁用JDBC自动提交,让Hibernate批量处理事务 spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true # 关闭二级缓存(如果不需要) spring.jpa.properties.hibernate.cache.use_second_level_cache=false
3. 优化CSV解析方式
避免使用String.split()处理超大CSV,改用专门的CSV解析库(如OpenCSV),减少内存开销:
import com.opencsv.CSVReader; import java.io.FileReader; // 替换原BufferedReader为CSVReader try (CSVReader reader = new CSVReader(new FileReader(path))) { reader.readNext(); // 跳过表头 String[] split; while ((split = reader.readNext()) != null) { RecordEntity record = new RecordEntity(); record.prop0 = split[0]; record.prop1 = split[1]; record.prop2 = split[2]; records.add(record); // 批量插入逻辑同上 } }
4. 调整JVM堆内存
根据服务器内存情况,增大堆内存上限,例如启动参数:
-Xmx8g -Xms4g
(如果服务器有16G内存,可设置-Xmx12g,根据实际情况调整)
疑问解答
- BufferedReader的行是否未释放?:单看
BufferedReader的line变量,每次循环会被新值覆盖,旧对象本应被GC回收,但由于实体对象持有split后的字符串引用,而实体被Hibernate缓存持有,导致这些字符串无法被回收,间接占用内存。 - Hibernate在saveAll后是否仍持有记录?:是的,Hibernate的Session一级缓存默认会保留所有持久化状态的实体,直到Session被手动清理或关闭。如果不执行
clear(),这些实体将一直占用堆内存,最终导致溢出。
内容的提问来源于stack exchange,提问作者Jack J
相关产品推荐
相关产品推荐

