通过JDBC批量插入记录时出现内存溢出问题求助
老兄,你的问题我太熟了——手动拼接INSERT SQL然后逐条执行,攒够数再commit的写法,在数据量小的时候没问题,但到1000万级别的数据,必然会爆内存!核心问题有两个:一是你没用到JDBC真正的批量操作API,驱动会把所有执行过的SQL都缓存起来(为了事务回滚),哪怕commit了也不会释放;二是SQLite的ResultSet默认会把全量数据加载到内存,1000万条直接就把内存撑爆了。
给你一套完整的解决方案,亲测有效:
1. 改用JDBC PreparedStatement的批量操作
别再手动拼SQL了!用预编译的PreparedStatement+addBatch()/executeBatch(),这才是JDBC设计的批量插入姿势,不仅内存占用低,执行效率也高。
2. 限制ResultSet的内存占用
SQLite JDBC驱动默认会把整个查询结果加载到内存,必须设置fetchSize让它分批从数据库取数据,避免一次性加载1000万条。
修改后的完整代码
import java.sql.*; import java.util.Collections; public void transfer() throws IOException, SQLException { String[] cols = parser(propertyPath); // 获取表字段 String query = "select * from " + originTable; // 预编译插入SQL,用?作为占位符 String insertSql = "insert into " + targetTable + " (" + String.join(",", cols) + ") values (" + String.join(",", Collections.nCopies(cols.length, "?")) + ")"; // 用try-with-resources自动关闭资源,避免泄漏 try (Statement originStmt = originDBOperate.createStatement(); ResultSet rs = originStmt.executeQuery(query); PreparedStatement targetPstmt = targetDBOperate.getConnection().prepareStatement(insertSql)) { // 设置ResultSet分批加载,每次取10000条 originStmt.setFetchSize(10000); // 关闭自动提交,开启批量模式 targetDBOperate.setCommit(false); int count = 0; while (rs.next()) { // 逐个设置参数 for (int i = 0; i < cols.length; i++) { Object value = rs.getObject(cols[i]); targetPstmt.setObject(i + 1, value); } // 添加到批量队列 targetPstmt.addBatch(); count++; // 每10000条执行一次批量插入并提交 if (count % 10000 == 0) { targetPstmt.executeBatch(); targetDBOperate.commit(); // 可选:清空批量队列(executeBatch后会自动清空,这里只是明确操作) targetPstmt.clearBatch(); } } // 处理剩余不足10000条的记录 if (count % 10000 != 0) { targetPstmt.executeBatch(); targetDBOperate.commit(); } } catch (SQLException e) { // 出错时回滚事务 targetDBOperate.rollback(); throw e; } finally { targetDBOperate.closeConnection(); } }
关键优化点解释
- PreparedStatement批量操作:复用同一个预编译SQL,只缓存参数,不会像手动拼SQL那样产生大量字符串对象,GC可以及时回收参数资源,内存占用极低。
- ResultSet fetchSize设置:强制SQLite驱动分批加载数据,每次只把10000条数据放到内存,不会一次性加载全量1000万条。
- try-with-resources:自动关闭
Statement、ResultSet、PreparedStatement,避免资源泄漏导致的内存占用。
额外注意事项
- 如果你的SQLite JDBC驱动版本较老,可能需要在连接URL中添加参数
&sqlite_jdbc.max_row_cache_size=10000来配合fetchSize生效,确保ResultSet不会偷偷加载全量数据。 - 可以根据服务器内存情况调整批量大小(比如改成20000或5000),找到内存和效率的平衡点。
- 如果originDB和targetDB是同一个SQLite实例,直接用
INSERT INTO targetTable SELECT * FROM originTable会快得多,完全不需要Java代码中转,但如果是不同实例,就只能用上面的批量方案。
这样修改后,内存占用会稳定在一个较低的水平,不会持续上涨到OOM,亲测迁移1000万条数据完全没问题!
内容的提问来源于stack exchange,提问作者nail fei
相关产品推荐
相关产品推荐

