流式查询是解决海量数据OOM的根本方案:通过JDBC游标逐行拉取、Java逐个处理并及时释放,内存稳定在KB级;需配置useCursorFetch=true、FORWARD_ONLY结果集类型及Integer.MIN_VALUE的fetchSize,并用selectCursor()配合for-each遍历,务必显式close()。

直接查几百万条数据不加限制,堆内存撑爆是必然结果——不是 JVM 配置小,而是加载方式错了:对象全进堆、缓存不释放、连接不关,OOM 只是时间问题。
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
优先用流式查询,而不是调大堆内存
流式是真正治本的方案:让 JDBC 用游标逐行拉取、Java 逐个处理、用完即丢,内存占用稳定在 KB 级。
- MySQL 连接 URL 必须加
useCursorFetch=true,否则fetchSize不生效 - SQL 声明需指定
resultSetType="FORWARD_ONLY"和resultSetConcurrency="CONCUR_READ_ONLY" - MyBatis Mapper 中设
fetchSize="-2147483648"(即Integer.MIN_VALUE),不能写成字符串或正数 - Java 侧调用
sqlSession.selectCursor()获取Cursor<t></t>,用 for-each 遍历,每轮只持有一个对象引用 - 务必在
finally或 try-with-resources 中显式close(),否则连接泄漏风险极高
分页要用对场景,别误当万能解
分页适合用户端列表、导出预览等可接受“分批查看”的场景,但必须避开常见陷阱:
- 禁用 MyBatis 的
RowBounds逻辑分页,它仍会全表扫描+全量加载到内存 - 用数据库原生物理分页(如
LIMIT offset, size),并确保ORDER BY字段有索引 - 深度分页(如
OFFSET > 100000)性能断崖下跌,应改用游标分页(基于id > last_id或create_time > ?) - MyBatis-Plus 分页插件要关闭一级缓存累积:
pagination.innerCache=false
Hibernate 要额外防“透明吃内存”
Hibernate 比 MyBatis 更容易悄悄耗尽堆内存:
- 显式禁用二级缓存:
@Cache(usage = CacheConcurrencyStrategy.NONE) - 避免
session.find()或getResultList(),改用session.scroll()或session.stream()(5.2+) - 关闭一级缓存:用
StatelessSession,或每批处理后调session.clear() - 实体映射精简:删掉不必要的
@Lob、@ElementCollection、级联关系,减少对象图深度
上线前必须加硬性防护
靠人盯代码不现实,得靠机制兜底:
- DAO 层拦截
findAll()、listAll()等无参全量方法,直接抛异常,强制走带分页/流式的入口 - SQL 审计接入 CI,用 Druid 规则自动拦截无
WHERE、无LIMIT、预估返回超 5000 行的语句 - 测试环境压测时开启
-XX:+HeapDumpOnOutOfMemoryError,复现后用 MAT 查 dominator tree,重点看ArrayList、实体类、ResultSetImpl的 GC Roots 引用链
不复杂但容易忽略。

















