流式过滤内存暴增主因是collect()破坏惰性求值,致全量加载;应改用foreach分批消费、skip/limit切片或jdbc fetchsize,避免filter依赖可变状态导致谓词失效。

流式过滤时内存暴增的典型表现
用 filter() + collect() 一次性拉取全部数据,哪怕源是 Stream,只要中间有 toList() 或转成 ArrayList,就会把整个结果集加载进堆——尤其当原始集合是千万级 ResultSet、文件行流或 HTTP 分块响应时,OutOfMemoryError: Java heap space 几乎必然发生。
根本问题不在 filter() 本身,而在后续「收集动作」破坏了流的惰性。Java 的 Stream 是懒求值的,但一旦调用 collect(Collectors.toList()),就强制触发全量计算和内存驻留。
分批处理必须绕开 collect() 的三种做法
核心原则:不攒结果,只做原子操作;让每批数据在作用域内自然释放。
- 用
forEach()直接消费,比如写入文件或发 HTTP 请求:stream.limit(1000).forEach(record -> sendToApi(record)); - 用
skip()+limit()手动切片(适合已知总长的List):list.stream().skip(batch * 1000).limit(1000).filter(...).forEach(...); - 对数据库游标类场景,直接用 JDBC 的
setFetchSize(1000)配合while (rs.next()),比用Stream.generate()模拟更稳——后者容易因闭包持有外部引用导致 GC 不掉
filter 后接分页的陷阱:谓词提前失效
如果过滤条件依赖外部可变状态(比如时间窗口、计数器、缓存命中),在分批中反复创建新 Stream 会导致状态重置。例如:
int count = 0; list.stream().filter(x -> ++count <= 100).forEach(...); // 第一批后 count=100,第二批又从 0 开始
正确做法是把状态抽到循环外,或改用 AtomicInteger:
- 用
AtomicInteger替代局部变量:AtomicInteger counter = new AtomicInteger(); stream.filter(x -> counter.getAndIncrement() - 避免在
filter中做 I/O 或锁操作——它可能被并行流多次调用,且无序 - 若需按业务逻辑分组再过滤(如“每个用户最多取 5 条”),别硬塞进
filter,改用Collectors.groupingBy()+ 手动截断
分批大小不是越大越好,1000 是个经验拐点
批量设为 10000 看似吞吐高,但实际容易卡在 GC STW 或网络超时;设为 10 又导致频繁 I/O 调度开销。测试发现,100–1000 是多数场景的平衡带:
- 数据库分页:MySQL 的
LIMIT 1000基本不触发 filesort,超过易走全表扫描 - HTTP 批量接口:多数服务端限制单请求 payload ≤ 1MB,1000 条 JSON 对象通常刚好卡在边界
- GC 压力:单批对象生命周期短于 Young GC 周期,能被快速回收;过大则进入老年代,引发 Full GC
真正难的是动态调优——比如上游数据倾斜时,某批含大量空记录,实际有效条数远低于预期。这时候得在批处理循环里加 if (actualCount == 0) break; 主动退出,而不是死等固定轮数。










