直接原因是数据库查询未加limit或分页导致百万级记录全加载进内存引发堆溢出;需通过异常日志、sql监控、线程快照和堆dump定位问题sql与代码,修复应优先加分页、改流式处理并引入sql审计。

直接原因是数据库查询没加 limit 或分页,一次拉取百万级记录全加载进内存,对象实例瞬间撑爆堆空间。这类问题在上线后高并发或数据量突增时极易暴露,但根子在代码逻辑和 SQL 设计上。
快速确认是不是查询导致的溢出
先看异常日志里有没有明确线索:
- 错误类型是否为 java.lang.OutOfMemoryError: Java heap space(不是 Metaspace 或 StackOverflow)
- 堆栈里是否出现 JDBC ResultSet、MyBatis Mapper、JPA Repository.findAll() 或类似数据读取方法调用
- 异常触发点是否在某个批量导出、同步、报表生成等明显涉及“全表扫描”或“无条件查询”的接口里
定位具体哪条 SQL 和哪段代码
不要只看日志堆栈,要结合运行时上下文:
- 查应用日志中该异常发生前几秒的 SQL 打印(如 MyBatis 开启
logging.level.org.apache.ibatis=DEBUG) - 用
jstack <pid></pid>抓线程快照,找处于RUNNABLE状态且正在执行数据库读取的线程,看其调用链 - 如果用了 Druid 或 HikariCP,打开连接池的 SQL 监控(如 Druid 的
druid.stat.mergeSql=true),查执行时间长、返回行数异常高的 SQL
验证并复现问题
在测试环境模拟真实压力,避免盲目调大堆内存掩盖问题:
- 用相同参数调用疑似接口,观察堆内存是否在几秒内飙升(用
jstat -gc <pid> 1000</pid>实时看 Eden/Old 区增长) - 开启
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/oom.hprof,复现后用 MAT 打开 dump 文件,按 dominator tree 查占比最高的类——大概率是ArrayList、HashMap或你实体类本身,再看其 GC Roots 引用链,基本能锁定到 DAO 层某次 findAll() 调用
修复与预防措施
改代码比调 JVM 参数更治本:
- 所有非必要全量查询必须加 分页(Pageable)或 limit,哪怕默认查 1000 条也比不设强
- 大数据量导出改用 流式处理:MyBatis 用
Cursor,JDBC 用setFetchSize(Integer.MIN_VALUE)+ 迭代处理,避免一次性加载 - 在 DAO 层或通用查询工具类里加硬性约束,比如拦截
findAll()调用并抛异常,强制走带分页的方法 - SQL 审计接入 CI 流程,用工具(如 Alibaba Druid 的 SQL 防御规则)自动检测无 WHERE、无 LIMIT、返回行数预估超阈值的语句










