目录
[1. 拆分大对象(Split Large Objects)](#1. 拆分大对象(Split Large Objects))
[示例:坏代码 vs 好代码](#示例:坏代码 vs 好代码)
[❌ 坏代码(一次性加载)](#❌ 坏代码(一次性加载))
[✅ 好代码(分批加载 + 拆分)](#✅ 好代码(分批加载 + 拆分))
[2. 流式处理(Stream / 流式读取)](#2. 流式处理(Stream / 流式读取))
[Java 中两种流式写法:](#Java 中两种流式写法:)
[① 数据库流式查询(MyBatis / JDBC)](#① 数据库流式查询(MyBatis / JDBC))
[✅ 正确写法(MyBatis 流式查询)](#✅ 正确写法(MyBatis 流式查询))
[② Java8 Stream 流式处理(不存大 List)](#② Java8 Stream 流式处理(不存大 List))
[❌ 坏代码](#❌ 坏代码)
[✅ 流式处理(不创建临时大集合)](#✅ 流式处理(不创建临时大集合))
[3. 不一次性加载全量数据(核心!)](#3. 不一次性加载全量数据(核心!))
[示例:Excel 导入优化(超级经典)](#示例:Excel 导入优化(超级经典))
[❌ 坏代码(一次性读取全部行)](#❌ 坏代码(一次性读取全部行))
[✅ 好代码(流式逐行读取)](#✅ 好代码(流式逐行读取))
[场景:导出 10 万用户数据(不 OOM、不卡顿)](#场景:导出 10 万用户数据(不 OOM、不卡顿))
这是生产环境解决 OOM、频繁 GC、大对象卡顿 的终极三板斧 。本文会用最通俗的语言 + 真实业务场景 + 可直接复制的代码,把这三个优化点讲透。
一、先讲核心思想(一句话记住)
不要把所有数据一次性全部加载到内存里,不要创建超大对象 / 超大集合,要像 "水管流水" 一样,读一点、处理一点、释放一点。
这就是:流式处理 + 分页 / 游标读取 + 小对象处理
二、为什么要这么优化?(生产血泪教训)
坏代码的后果:
- 一次性加载 10w 条数据到 List → 占几百 MB 内存
- 超大对象(10MB+) → 直接进入老年代
- FullGC 频繁 → 接口超时、服务卡死
- 最终 OOM 崩溃
好代码的效果:
- 内存占用始终稳定
- 无大对象
- YGC 少,FGC 几乎为 0
- 处理百万数据也不崩
三、三个优化点详细讲解
1. 拆分大对象(Split Large Objects)
什么是大对象?
- 单个对象 > 10MB
- 超大 List/Map 装几万条数据
- 一次性加载整个文件、Excel、大报文
为什么危险?
JVM 规则:大对象直接进入老年代,老年代满了就触发 FullGC。
怎么拆分?
- 大集合拆成小批量(100~1000 条一批)
- 大报文拆字段,不需要的不加载
- 大文件分段读,不一次性读入内存
示例:坏代码 vs 好代码
❌ 坏代码(一次性加载)
// 一次性加载10万条,占巨大内存
List<User> userList = userMapper.selectAll();
for(User user : userList){
// 处理
}
✅ 好代码(分批加载 + 拆分)
// 每次只查1000条
int pageSize = 1000;
int pageNum = 1;
while(true){
List<User> userList = userMapper.selectPage(pageNum, pageSize);
if(userList.isEmpty()) break;
// 处理小批量数据
processList(userList);
pageNum++;
}
2. 流式处理(Stream / 流式读取)
什么是流式处理?
数据像水流一样,来一条处理一条,不全部存内存。
优势:
- 内存占用极低
- 无大集合
- 处理完立即释放
Java 中两种流式写法:
① 数据库流式查询(MyBatis / JDBC)
✅ 正确写法(MyBatis 流式查询)
// Mapper 接口(注解方式)
@Select("SELECT * FROM user")
@ResultType(User.class)
Cursor<User> selectAllUserStream(); // Cursor = 流
// 业务代码:逐条读取,不加载全量
try (Cursor<User> cursor = userMapper.selectAllUserStream()) {
for (User user : cursor) {
// 逐条处理,内存只存1条
handleUser(user);
}
}
内存几乎不涨!
② Java8 Stream 流式处理(不存大 List)
❌ 坏代码
List<String> nameList = new ArrayList<>();
for(User user : userList){
nameList.add(user.getName());
}
✅ 流式处理(不创建临时大集合)
userList.stream()
.map(User::getName)
.forEach(this::sendMessage); // 直接处理,不存储
3. 不一次性加载全量数据(核心!)
错误做法:
List data = queryAll(); // 全量加载 → OOM源头
正确做法:
- 分页查询
- 流式查询
- 游标 / 迭代器读取
- 文件分段读取
示例:Excel 导入优化(超级经典)
❌ 坏代码(一次性读取全部行)
List<Row> rows = sheet.getRows(); // 加载10万行到内存
for(Row row : rows){
// 处理
}
✅ 好代码(流式逐行读取)
// 一行行读,读完即释放,内存只存一行
ExcelReader reader = ExcelUtil.getReader(file);
reader.read(this::processRow); // 流式消费
// 处理方法
void processRow(UserRow row){
// 处理单行
}
四、三合一终极实战代码(生产标准模板)
场景:导出 10 万用户数据(不 OOM、不卡顿)
@Transactional(readOnly = true) // 流式查询必须开启事务
public void exportUser(OutputStream outputStream){
// 1. 数据库流式查询(不加载全量)
try (Cursor<User> cursor = userMapper.streamAllUser()) {
// 2. 流式写入Excel(不创建大对象)
ExcelWriter writer = ExcelUtil.getWriter(outputStream);
writer.write(cursor.stream() // 3. Java流式处理
.map(this::convertToVO)
.peek(item -> {
// 逐条处理
}));
writer.flush();
}
}
这个代码的优势:
- 内存永远只占用几十 KB
- 不会产生大对象
- 不会频繁 GC
- 处理 100 万数据也不会 OOM
五、这三个优化解决了什么生产问题?
- OOM
- 频繁 FullGC
- 大对象导致卡顿
- 内存持续上涨
- 大数据量接口超时
六、最简单记忆口诀
不一次性加载, 不创建大对象, 用流式一条条处理。