You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将30GB大CSV导入H2数据库时遭遇Java堆内存溢出问题

超大CSV导入H2堆内存溢出问题排查与解决

核心原因

  1. Hibernate一级缓存堆积:saveAll执行后,Hibernate的Session(一级缓存)会默认保留所有持久化实体对象,不会自动清理,累积15次100万条后,缓存中的实体占用大量堆内存。
  2. 字符串与对象引用未释放:CSV解析生成的字符串(split数组、实体属性)被缓存中的实体持有,导致这些对象无法被GC回收,进一步加剧内存占用。
  3. 批量插入配置缺失:默认Spring Data JPA未开启真正的批量插入,Hibernate可能单条执行SQL,且未及时刷新会话,导致缓存无法释放。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 10:47:33