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

直接查几百万条数据不加限制,堆内存撑爆是必然结果——不是 JVM 配置小,而是加载方式错了:对象全进堆、缓存不释放、连接不关,OOM 只是时间问题。
优先用流式查询,而不是调大堆内存
流式是真正治本的方案:让 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,用 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 引用链
不复杂但容易忽略。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










