You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 14:25:41