流式查询是处理百万级数据避免OOM的核心方案:通过JDBC游标逐行读取,禁用MyBatis全量映射,配合useCursorFetch=true、fetchSize设置及Cursor显式关闭,实现内存常量级占用。

直接用 MyBatis 或 Hibernate 一次性查几百万条数据,不加控制,堆内存很快就会撑爆。这不是 JVM 配置小的问题,而是数据加载方式本身不合理——对象全进堆、缓存不释放、连接不关闭,OOM 是必然结果。
流式查询:让数据“边读边处理”,不囤积
核心是绕过 MyBatis 默认的全量映射机制,用 JDBC 底层的流式能力逐行消费结果。
- Mapper XML 中设置 fetchSize="1000"(数值按网络与业务吞吐权衡,通常 500–2000)
- MySQL 连接 URL 必须加上 useCursorFetch=true,否则 fetchSize 不生效
- SQL 查询需配合 resultSetType="FORWARD_ONLY" 和 resultSetConcurrency="CONCUR_READ_ONLY"
- Java 侧用 SqlSession.selectCursor() 获取 Cursor 对象,再用 for-each 遍历,每轮只持有一个对象引用
- 务必在 finally 块或 try-with-resources 中显式 close Cursor,避免连接泄漏
分页不是万能解,但必须用对场景
分页适用于导出、后台列表等用户可接受“分批查看”的场景,但不能简单套用逻辑分页(如 RowBounds),它仍会执行全表扫描并加载全部结果到内存。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 优先采用数据库原生物理分页,例如 MySQL 的 LIMIT offset, size,确保只返回目标数据
- MyBatis-Plus 分页插件要开启 pagination.innerCache=false,禁用一级缓存累积
- 避免深度分页(如 OFFSET 超过 10 万),改用基于游标的分页(如 last_id 或 create_time > ?)
- 导出类任务慎用 Page<T>,它默认把所有记录装入 List<T>,应改用流式 + 分块写文件
Hibernate 的特殊风险点要主动规避
Hibernate 比 MyBatis 更容易因透明性导致 OOM:一级/二级缓存、懒加载代理、脏检查机制都会悄悄吃内存。
立即学习“Java免费学习笔记(深入)”;
- 大批量查询时,禁用二级缓存(@Cache(usage = CacheConcurrencyStrategy.NONE))
- 禁用 Session 级别的一级缓存,用 session.clear() 或创建无状态 session(StatelessSession)
- 避免使用 getResultList(),改用 scroll() 或 stream()(Hibernate 5.2+)
- 实体映射尽量精简,去掉无关的 @Lob、@ElementCollection 等重量级字段
- 批量更新/插入时,每处理 50–100 条调用一次 session.flush() + session.clear()
配套基础设施不能少
光改代码不够,数据库连接池、JDBC 驱动、JVM 参数都要协同调整。
- HikariCP 连接池配置 maxLifetime=1800000(30 分钟),防止长连接下 ResultSet 持久占用
- MySQL 驱动建议用 8.0.33+,老版本对 useCursorFetch 支持不稳定
- JVM 启动参数增加 -XX:+HeapDumpOnOutOfMemoryError,便于事后定位是哪类对象堆积
- 监控关键指标:JDBC ResultSetImpl 实例数、堆内 String / Entity 对象数量、GC 频率与耗时

















