二次调用@Async处理Excel方法时出现Java堆内存溢出问题
问题分析与解决方案
首先,不需要手动清理堆内存,Java的GC会自动管理内存,但你的代码存在资源未正确释放和潜在内存泄漏的问题,导致第二次调用时堆内存耗尽。以下是具体问题和修复方案:
1. 核心问题:POI资源未关闭
你的代码中OPCPackage没有关闭,这是POI处理Excel时的重量级对象,会占用大量堆内存。即使你关闭了文件流和sheet流,OPCPackage不关闭的话,内部的内存映射、缓存数据不会被释放;尤其@Async方法在独立线程执行,这些对象会被线程持有,GC无法及时回收。
2. 具体修复步骤
(1)用try-with-resources自动管理资源
将OPCPackage、sheet流等资源放在try-with-resources块中,确保代码执行完毕或异常时自动关闭:
@Async // 移除@Transactional:@Async方法在独立线程执行,事务上下文不会传递,该注解无效 public void load(InputStream file, boolean save, boolean deleteYear) throws IOException { try (OPCPackage pkg = OPCPackage.open(file); // 自动关闭OPCPackage InputStream sheet2 = new XSSFReader(pkg).getSheet("rId1")) { // 自动关闭sheet流 SharedStringsTable sst = new XSSFReader(pkg).getSharedStringsTable(); XMLReader parser = fetchSheetParser(sst); InputSource sheetSource = new InputSource(sheet2); parser.parse(sheetSource); SheetHandlerService sheetHandlerService = (SheetHandlerService) parser.getContentHandler(); LOG.info("end processing data"); if (deleteYear) { deleteTable(); } if (save) { saveCorrect(); saveErrors(); } // 清理Handler中的缓存数据 sheetHandlerService.clearCachedData(); // 假设Handler有这个方法,清空内部集合 } catch (Exception e) { LOG.error("处理Excel失败", e); // 打印完整堆栈,不要只打消息 } }
(2)移除无效的@Transactional注解
@Async方法会在Spring创建的异步线程池中执行,和原请求线程的事务上下文完全隔离,所以你的@Transactional注解根本不会生效,反而可能导致不必要的线程资源浪费,直接移除即可。
(3)清理SheetHandlerService的缓存
如果你的SheetHandlerService在解析Excel时缓存了大量行数据(比如用List存储所有解析结果),处理完saveCorrect()和saveErrors()后,必须手动清空这些缓存集合,让GC能回收这部分内存:
// 在SheetHandlerService中添加清理方法 public void clearCachedData() { if (this.correctData != null) { this.correctData.clear(); this.correctData = null; // 可选,帮助GC更快回收 } if (this.errorData != null) { this.errorData.clear(); this.errorData = null; } }
(4)配置合理的@Async线程池
默认的Spring异步线程池是无界的(SimpleAsyncTaskExecutor),如果短时间内多次调用,会创建大量线程,每个线程持有POI资源,快速耗尽内存。建议自定义线程池:
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); // 根据CPU核心数设置 executor.setMaxPoolSize(8); executor.setQueueCapacity(10); // 任务队列大小,避免无界队列 executor.setThreadNamePrefix("ExcelProcessor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
(5)优化POI解析的内存占用
如果Excel文件极大,即使SAX解析也可能因为缓存数据过多导致内存问题,可以:
- 在
SheetHandlerService中分批保存数据,解析一批就保存一批,不要缓存全部数据 - 禁用POI的不必要缓存,比如设置
POIXMLDocument.setCacheMemoryPackage(false)
3. 额外排查点
- 检查
saveCorrect()和saveErrors()方法是否存在内存泄漏(比如未关闭数据库连接、缓存了大量查询结果) - 监控堆内存使用(用jmap、jvisualvm工具),确认内存泄漏的对象类型,精准定位问题
内容的提问来源于stack exchange,提问作者MrSman
相关产品推荐
相关产品推荐

