根因一句话:**Java heap 空间耗尽,导致 MyBatis 批量提交时 MySQL 驱动在拼接/拷贝 SQL 字符串的过程中申请不到内存,最终抛 `OutOfMemoryError

根因一句话:**Java heap 空间耗尽,导致 MyBatis 批量提交时 MySQL 驱动在拼接/拷贝 SQL 字符串的过程中申请不到内存,最终抛 `OutOfMemoryError: Java heap space`**。
## 一、现象复盘
1. 业务代码调用 `MybatisBatchUtil.asyncBatch(...)` 做异步批量写库。
2. MyBatis 底层使用 `BatchExecutor`,每 1000 条(或配置值)拼成一条多值 `INSERT ... VALUES (...),(...)`。
3. 当一次批量数据量过大、字段很长(如 JSON、BLOB、长文本)或 JVM 堆本身已接近占满时,MySQL 驱动在 `QueryInfo` 里把 SQL 字符串 `getBytes()` 时申请了一块接近 SQL 长度的 `byte[]`。
4. 申请失败,触发 `OutOfMemoryError`, Druid 捕获后转成 `SQLException`,MyBatis 再包装成 `PersistenceException` 向上抛。
## 二、快速止血方案(秒级生效)
1. **立刻把 batch 大小降到 50~100**
代码里搜索 `MybatisBatchUtil.asyncBatch`,把 `batchSize` 参数或 `@BatchSize` 改成 50。
2. **如果用的是 MySQL,关闭 rewriteBatchedStatements 的“超长 SQL” 模式**
JDBC URL 加上 `&rewriteBatchedStatements=false&useServerPrepStmts=true`,驱动会走真正的服务器端预编译,不再拼出超长 SQL,内存占用骤降。
3. **临时扩容堆**
启动脚本里把 `-Xmx` 调大(例如 2G→4G),重启节点,先抗住高峰。
## 三、根治方案
1. **控制批量粒度**
- 推荐 100~300 条一批,字节量 < 2 MB;如果单条就几十 KB,直接 50 条一批。
- 提供工具方法:根据“预估 SQL 长度”动态拆批,而不是固定条数。
2. **流式/分页查询 + 分批提交**
如果“一次性查 20 万条到内存再批量写”是前置逻辑,务必改成游标或分页,边读边写。
3. **JVM 级**
- 保留 20 % 堆安全垫:`-Xmx` 至少比“正常批量峰值”大 30 %。
- 打开 OOM 自动 dump:`-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/logs/oom.hprof`,方便复盘。
- GC 算法:8 G 以下堆用 ParallelGC,8 G 以上用 G1,并设置 `-XX:+UseStringDeduplication` 减少字符串重复。
4. **MySQL 驱动层**
- 5.1.x 驱动本身有 bug,超长 SQL 会反复拷贝数组,建议升到 8.0.33+。
- 若必须保留 `rewriteBatchedStatements=true`,则同时打开 `maxAllowedPacket=32M` 并严格控制 batch 条数。
5. **代码层**
- 大字段(json、text、blob)不要参与 batch,可拆到单独表或异步消息队列。
- 批量写入前,先用 `SqlSession.flushStatements()` 手动刷一次,避免一次性积压太多。
## 四、一分钟自查清单
| 检查项 | 期望结果 |
|--------|----------|
| `-Xmx` 实际值 | ≥ 业务峰值 *1.3 |
| `batchSize` 值 | ≤ 300 |
| JDBC URL 里 `rewriteBatchedStatements` | false(或条数 <200) |
| MySQL 驱动版本 | ≥ 8.0.22 |
| 单条记录平均大小 | < 20 KB |
| 是否出现“20 万条一次查” | 否,已改分页/游标 |
## 五、小结
这次不是“SQL 写错”,也不是“Druid 泄漏”,纯粹是“一次批量 SQL 太大 + 堆太小”把内存打爆。
**先降 batch 大小 → 改 rewriteBatchedStatements → 调大堆 → 长期优化分页/拆批/升级驱动**,即可彻底解决。
更多推荐



所有评论(0)