应优先检查sql是否跳过分页、禁用流式读取或全量加载:确认mybatis未漏写fetchsize、jpa未误用findall()、jdbc未忽略setfetchsize,结合oom堆栈与慢日志定位元凶sql。

直接查数据库语句和应用层调用链,重点看是否跳过了分页、禁用了流式读取、或把全量结果塞进了静态集合。
快速定位问题SQL与执行上下文
从错误堆栈入手,找到抛出 OutOfMemoryError: Java heap space 的线程和方法(如 ExportController.exportData() 或 ReportService.generate())。顺着调用栈向上,确认触发查询的 SQL 是否含 LIMIT、TOP 或分页参数;检查 MyBatis 的 <select></select> 标签是否漏写 fetchSize="-2147483648",或 JPA 中是否误用 findAll() 而非 findAll(Pageable)。同时抓取慢日志或数据库审计日志,筛选执行时间长、扫描行数超 10 万的语句——这类语句大概率就是“没加限制”的元凶。
检查结果集处理方式是否全量加载
确认代码中是否将 ResultSet 或 List 直接转成内存驻留结构:
- MyBatis:是否用了
selectList("xxx")返回List<map></map>或List<entity></entity>,且未配置fetchSize或禁用二级缓存 - Hibernate:是否调用
session.createQuery(...).list(),而非.scroll(ScrollMode.FORWARD_ONLY)或Stream<t></t> - JDBC:是否调用
statement.executeQuery(sql)后,用while(rs.next())循环但未设statement.setFetchSize(Integer.MIN_VALUE) - 导出逻辑:是否用 Apache POI 的
XSSFWorkbook直接写百万行,而非切换为SXSSFWorkbook或 CSV 流式写出
验证是否存在跨批次引用或静态缓存放大
即使做了分页,若后续逻辑引入隐式放大,仍会溢出:
- 检查是否有
static List<object> cache = new ArrayList()</object>在每次查询后addAll(result),导致数据越积越多 - 确认 DTO 转换是否深拷贝了关联对象(如 User 带着 Address、Order 列表),单条记录实际内存占用翻数倍
- 排查日志打印:是否在 debug 级别输出了整个
List<map></map>,触发 Jackson 全量序列化树模型
用最小证据闭环验证
不依赖猜测,做三步实证:
- 在测试环境复现:用相同参数调用接口,通过
jstat -gc <pid></pid>观察 Young GC 频次突增、老年代使用率直线爬升 - 加 JVM 参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof,OOM 后用 MAT 打开,按 “Dominator Tree” 查最大对象,90% 情况下会指向一个超大ArrayList或HashMap,右键 “Path to GC Roots” 看谁在持有着它 - 临时改 SQL 加
LIMIT 1000,再测——若内存平稳、接口返回正常,即可 100% 锁定是未限流查询所致











