使用Apache POI读取50万行大Excel文件时应用程序卡住求助
解决超大Excel文件读取卡顿/内存溢出问题
你的问题根源在于常规POI Workbook会把整个Excel文件加载到内存,50万行100列的文件会瞬间占用大量内存,导致JVM在WorkbookFactory.create阶段就卡住,根本无法进入后续代码执行。以下是两种可行的解决方案:
方案一:使用SXSSFWorkbook(推荐,代码改动小)
SXSSF是POI针对XLSX文件的流式处理实现,它会将超出指定行数的数据写入临时磁盘文件,仅在内存中保留固定数量的行(称为"窗口大小"),能大幅降低内存占用。
修改后的核心代码示例:
public List<ExcelLine> getExcelLines(Path path, int batchSize) { log.info("Reading excel file"); List<ExcelLine> result = new ArrayList<>(); // 指定窗口大小,比如保留1000行在内存,超出的写入临时文件 try (SXSSFWorkbook workbook = new SXSSFWorkbook(1000); FileInputStream fis = new FileInputStream(path.toFile())) { workbook.setCompressTempFiles(true); // 压缩临时文件节省磁盘空间 SXSSFSheet sheet = (SXSSFSheet) workbook.getSheetAt(0); Iterator<Row> rowIterator = sheet.iterator(); // 跳过表头(如果需要) if (rowIterator.hasNext()) { rowIterator.next(); } int count = 0; while (rowIterator.hasNext()) { Row row = rowIterator.next(); ExcelLine line = parseRowToExcelLine(row); // 自定义行解析逻辑 result.add(line); count++; // 分批处理,避免集合过大 if (count % batchSize == 0) { // 处理当前批次数据(比如入库) processBatch(result); result.clear(); // 手动刷新临时文件,释放内存 sheet.flushRows(); } } // 处理剩余数据 if (!result.isEmpty()) { processBatch(result); } } catch (IOException e) { log.error("Read excel failed", e); throw new RuntimeException(e); } return result; } // 自定义行解析和批处理方法(示例) private ExcelLine parseRowToExcelLine(Row row) { ExcelLine line = new ExcelLine(); // 按列读取单元格数据 line.setCol1(row.getCell(0).getStringCellValue()); line.setCol2(row.getCell(1).getNumericCellValue()); // ... 其他列解析 return line; } private void processBatch(List<ExcelLine> batch) { // 比如写入数据库、生成报表等操作 log.info("Processing batch with {} records", batch.size()); }
注意事项:
- SXSSF仅支持XLSX格式(.xlsx),如果是旧版XLS(.xls)文件,需要使用POI扩展的流式读取工具。
- 临时文件默认存储在系统临时目录,可通过
workbook.setTempFileCreationStrategy()指定自定义目录,避免磁盘空间不足。
方案二:使用XSSF的SAX解析模式(极致内存优化)
如果SXSSF的内存占用仍然过高,可以直接使用SAX事件驱动解析,完全不构建内存中的DOM树,逐行处理Excel内容,内存占用几乎恒定。
核心代码示例:
public List<ExcelLine> getExcelLines(Path path, int batchSize) { log.info("Reading excel file"); List<ExcelLine> currentBatch = new ArrayList<>(); try (OPCPackage pkg = OPCPackage.open(path.toFile())) { XSSFReader reader = new XSSFReader(pkg); StylesTable styles = reader.getStylesTable(); ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg); XMLReader parser = XMLReaderFactory.createXMLReader(); // 自定义ContentHandler处理行和单元格事件 parser.setContentHandler(new ExcelContentHandler(styles, strings) { @Override protected void handleRow(RowData rowData) { ExcelLine line = parseRowDataToExcelLine(rowData); currentBatch.add(line); if (currentBatch.size() >= batchSize) { processBatch(currentBatch); currentBatch.clear(); } } }); // 解析第一个sheet InputStream sheetInputStream = reader.getSheet("rId1"); InputSource sheetSource = new InputSource(sheetInputStream); parser.parse(sheetSource); sheetInputStream.close(); // 处理剩余数据 if (!currentBatch.isEmpty()) { processBatch(currentBatch); } } catch (Exception e) { log.error("Read excel failed", e); throw new RuntimeException(e); } return currentBatch; } // 自定义ExcelContentHandler(简化示例) abstract class ExcelContentHandler extends DefaultHandler { private final StylesTable styles; private final ReadOnlySharedStringsTable strings; private StringBuilder cellContent; private RowData currentRow; private int currentCol; public ExcelContentHandler(StylesTable styles, ReadOnlySharedStringsTable strings) { this.styles = styles; this.strings = strings; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if ("row".equals(qName)) { currentRow = new RowData(); currentCol = 0; currentRow.setRowNum(Integer.parseInt(attributes.getValue("r"))); } else if ("c".equals(qName)) { cellContent = new StringBuilder(); } } @Override public void characters(char[] ch, int start, int length) { if (cellContent != null) { cellContent.append(ch, start, length); } } @Override public void endElement(String uri, String localName, String qName) { if ("c".equals(qName)) { currentRow.addCellValue(currentCol++, cellContent.toString()); cellContent = null; } else if ("row".equals(qName)) { // 跳过表头行(比如行号为1的情况) if (currentRow.getRowNum() > 1) { handleRow(currentRow); } currentRow = null; } } protected abstract void handleRow(RowData rowData); } // 辅助类:存储行数据 class RowData { private int rowNum; private List<String> cellValues = new ArrayList<>(); public void addCellValue(int col, String value) { cellValues.add(value); } public int getRowNum() { return rowNum; } public void setRowNum(int rowNum) { this.rowNum = rowNum; } public List<String> getCellValues() { return cellValues; } }
注意事项:
- SAX解析需要手动处理XML事件,代码复杂度较高,但内存占用极低,适合超大规模文件。
- 需要额外处理不同单元格类型(字符串、数字、日期、公式等),避免解析错误。
内容的提问来源于stack exchange,提问作者Alexey
相关产品推荐
相关产品推荐

