使用Apache POI Stream API导出150K+数据时遭遇OutOfMemoryError
我完全懂你现在的困扰:明明用了专门处理大数据量的SXSSF组件,结果还是碰到了OOM,而且你不想靠调大Java堆内存这种“治标不治本”的方法来解决。咱们一步步拆解问题,找到根源和可行的解决方案。
可能的问题根源
先理清楚SXSSF的核心逻辑:它会把超出内存阈值的行写入临时文件,只保留指定数量的行在内存中。但如果你的实现有这些细节没注意,还是会导致内存爆掉:
- SXSSF默认内存保留行数过高:POI 3.14里SXSSFWorkbook默认会保留100行在内存里,数据量极大时这个阈值还是会让内存吃紧;
- 一次性加载全量数据到内存:你把数据库查询的所有结果都塞进
List<TransactionDTO>里,这相当于先把所有数据都占满内存,SXSSF的磁盘缓存优化完全没了用武之地; - 导出逻辑的隐藏内存消耗:比如重复创建单元格样式、大量字符串未复用,或者资源释放不彻底。
针对性解决方案
1. 手动缩小SXSSF的内存保留窗口
创建SXSSFWorkbook时,主动设置更小的内存保留行数,让更多行提前刷到磁盘:
// 只保留50行在内存,超出部分直接写入临时文件 SXSSFWorkbook wb = new SXSSFWorkbook(50); // 如果你用的ExportRevisionResponseExcel是封装方法,一定要确保内部是这么创建SXSSFWorkbook的
默认的100行对于超大数据量来说还是偏多,调小后能显著降低内存占用。
2. 分批查询+分批写入(核心优化)
这是解决问题的关键!你现在一次性加载全量数据到List的操作,直接把SXSSF的优化逻辑架空了。改成分批查询数据库,写一批就释放一批数据:
// 假设你的transactionService支持分页查询 int pageSize = 1000; // 每批加载1000条,可根据内存情况调整 int pageNum = 1; List<TransactionDTO> batchData; SXSSFWorkbook wb = new SXSSFWorkbook(50); Sheet sheet = wb.createSheet("Transaction Statistics"); // 先写入表头 Row headerRow = sheet.createRow(0); headerRow.createCell(0).setCellValue("Status"); headerRow.createCell(1).setCellValue("Request"); while ((batchData = transactionService.findByInDateRange(status, fromDate, toDate, pageNum, pageSize)) != null && !batchData.isEmpty()) { int startRowNum = (pageNum - 1) * pageSize + 1; // 表头占了第0行 // 写入当前批次数据 for (int i = 0; i < batchData.size(); i++) { TransactionDTO dto = batchData.get(i); Row row = sheet.createRow(startRowNum + i); row.createCell(0).setCellValue(dto.getStatus()); row.createCell(1).setCellValue(dto.getRequest()); } // 手动触发刷盘,让SXSSF把内存里的行写入临时文件 ((SXSSFSheet)sheet).flushRows(); // 清空当前批次,帮助GC快速回收内存 batchData.clear(); pageNum++; }
这样内存里只会保留当前批次的1000条数据+SXSSF的50行窗口,内存占用会大幅下降。
3. 复用单元格样式,减少冗余对象
如果你的导出逻辑里每次创建单元格都新建样式,会产生大量重复的样式对象占用内存。提前创建好样式,全局复用:
// 提前创建好要复用的样式 CellStyle cellStyle = wb.createCellStyle(); Font font = wb.createFont(); font.setFontName("Microsoft YaHei"); font.setFontHeightInPoints((short)12); cellStyle.setFont(font); cellStyle.setAlignment(HorizontalAlignment.CENTER); // 写入单元格时直接复用 row.createCell(0).setCellStyle(cellStyle);
4. 确保资源释放彻底
你已经调用了wb.dispose()和wb.close(),但最好把资源释放逻辑放到finally块里,避免异常时资源泄漏:
ByteArrayOutputStream outByteStream = null; OutputStream outStream = null; try { outByteStream = new ByteArrayOutputStream(); wb.write(outByteStream); byte[] outArray = outByteStream.toByteArray(); // 设置response的相关参数... outStream = response.getOutputStream(); outStream.write(outArray); outStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (wb != null) { wb.dispose(); // 释放SXSSF的临时文件 wb.close(); } if (outByteStream != null) outByteStream.close(); if (outStream != null) outStream.close(); } catch (IOException e) { e.printStackTrace(); } }
5. 升级POI版本(可选)
POI 3.14是2015年的老版本,后续的4.x、5.x版本对SXSSF的内存管理做了不少优化,比如修复了一些内存泄漏问题、优化了临时文件的处理逻辑。如果项目允许,升级版本能从底层减少内存问题的概率。
总结
最核心的问题是你一次性加载了全量数据到内存,让SXSSF的磁盘缓存机制完全发挥不了作用。先改成分批查询+分批写入,再配合调小SXSSF的内存窗口,应该就能解决OOM问题了。
内容的提问来源于stack exchange,提问作者Sanatbek Matlatipov

