callable本身不解决oom,但通过配合jvm调优、数据流控制和资源约束,可将大任务拆为小而稳的单元;需按堆容量匹配分片粒度,单任务内存≤堆总容量10%~15%,禁用共享非线程安全对象,采用流式读写与及时释放,并联动线程池超时与内存水位监控。

Callable本身不解决OOM,但它为内存可控的并行处理提供了结构基础——关键在于如何用它配合JVM调优、数据流控制和资源约束,把“大任务”拆成“小而稳”的单元。
分片粒度要匹配堆容量,而非单纯追求并发数
盲目增加Callable任务数量,反而加剧GC压力。比如堆设为2GB,每片加载50万订单,若单条订单占2KB,则单片内存占用约1GB,两片并发就逼近极限。
- 按实际对象大小预估单片内存:用
JProfiler或VisualVM采样典型数据实体的 shallow heap + retained heap - 设置安全上限:单个Callable任务内存占用建议 ≤ 堆总容量的10%~15%,留足GC空间和线程栈开销
- 动态分片:对超长文本、嵌套JSON等变长数据,改用“按字节/行数”而非“按记录数”切分,避免某一片突然膨胀
禁止在Callable中持有全局非线程安全对象
静态工具类(如SimpleDateFormat)、共享连接池、未配置线程安全的ObjectMapper,会在多实例并发时引发隐式内存堆积或竞争锁导致任务阻塞,间接拖慢回收节奏。
- 日期处理统一用
DateTimeFormatter.ofPattern("...").withZone(ZoneId.systemDefault()),每次调用新建或复用immutable实例 -
ObjectMapper不要声明为static,每个Callable内创建局部实例,或使用ThreadLocal<objectmapper></objectmapper> - 数据库连接必须走连接池(如HikariCP),严禁在Callable里new Connection或长期持有ResultSet
流式读写+及时释放,让内存“只驻留必要数据”
OOM常发生在“查完再导”模式:JDBC全量拉取→内存List装满→POI生成Excel→一次性写出。Callable若沿用此流程,等于把压力从单线程转嫁到多个线程,总量未减。
- 查库侧:设置
statement.setFetchSize(500),配合数据库游标(MySQL加useCursorFetch=true),让ResultSet按需填充 - 导出侧:Excel用
SXSSFWorkbook,CSV用BufferedWriter逐行flush;写完立即dispose()工作簿,置空集合引用 - 每片处理完主动触发局部清理:
System.gc()不推荐,但可显式list.clear()、map = null,助GC识别可回收区域
线程池与超时策略必须联动JVM内存水位
固定大小线程池(如newFixedThreadPool(8))若任务持续超时,会堆积大量等待Future,其内部持有的结果对象和异常堆栈持续占用堆内存。
- 用
ThreadPoolExecutor替代Executors工厂方法,自定义RejectedExecutionHandler:当队列满时直接拒绝,而非无限制排队 - 提交时统一加总超时:
executor.invokeAll(tasks, 120, TimeUnit.SECONDS),避免单个慢任务卡死整批 - 监控堆内存使用率(如通过
MemoryUsage.getUsed() / MemoryUsage.getMax()),超过80%时自动降级并发数或暂停新任务提交
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











