异步压缩NIO Path集合时出现Java heap out of memory问题求助
堆内存溢出根因
你当前的压缩逻辑核心问题是将整个ZIP包全量缓存到堆内存中,并发场景下堆内存占用会随任务数、压缩文件总大小线性增长,最终触发OOM,具体问题点如下:
- 全量堆内缓存:你使用
ByteArrayOutputStream存储压缩后的全量ZIP二进制数据,调用toByteArray()时还会额外复制一份同等大小的字节数组,单个压缩任务就会占用「ZIP包体积 * 2」的堆内存,多并发处理大文件时会快速耗尽堆空间 - 资源泄漏隐患:代码中读取源文件的
FileInputStream没有主动关闭,长时间运行会累计文件句柄泄漏、堆内存溢出风险 - 无并发控制逻辑:未限制压缩任务的最大并行数,短时间内提交多个大文件压缩任务时,堆内存占用会直接超出上限
修复方案
方案1:边压缩边上传,无内存中转(推荐)
通过管道流实现压缩逻辑和SFTP上传并行,完全不需要在内存中缓存完整ZIP包:
public InputStream compressToZip(String s3folderName, Set<Path> paths) throws Exception { PipedInputStream pipedIn = new PipedInputStream(8192 * 1024); // 设置8M缓冲区 // 异步执行压缩逻辑,边压缩边写入管道流 CompletableFuture.runAsync(() -> { try (ZipOutputStream zos = new ZipOutputStream(new PipedOutputStream(pipedIn))) { for (Path path : paths) { zos.putNextEntry(new ZipEntry(path.getFileName().toString())); // try-with-resources自动关闭文件输入流 try (InputStream fis = Files.newInputStream(path)) { IOUtils.copy(fis, zos); } zos.closeEntry(); } } catch (Exception e) { // 自行补充异常处理逻辑 } }); return pipedIn; }
调用侧代码不需要做任何修改,直接使用返回的输入流写入SFTP即可。
方案2:临时文件中转(稳定性更高)
如果担心管道流的异步异常不好处理,可以将压缩包先写入本地临时文件,完全不占用堆内存,流关闭时自动删除临时文件:
public InputStream compressToZip(String s3folderName, Set<Path> paths) throws Exception { // 创建临时压缩文件 Path tempZipPath = Files.createTempFile("compress_temp_", ".zip"); try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(tempZipPath))) { for (Path path : paths) { zos.putNextEntry(new ZipEntry(path.getFileName().toString())); try (InputStream fis = Files.newInputStream(path)) { IOUtils.copy(fis, zos); } zos.closeEntry(); } } // 返回文件输入流,重写close方法实现流关闭后自动删除临时文件 return new FileInputStream(tempZipPath.toFile()) { @Override public void close() throws IOException { super.close(); Files.deleteIfExists(tempZipPath); } }; }
额外优化建议
- 压缩任务统一提交到固定核心线程数的线程池执行,限制最大并行数,避免并发过高导致内存、IO资源耗尽
- 若仍需要使用内存中转方案,可先统计待压缩文件总大小,超过阈值时自动降级到临时文件方案
- 所有IO操作均使用try-with-resources语法自动释放资源,避免泄漏
内容的提问来源于stack exchange,提问作者mwKART
相关产品推荐
相关产品推荐

