本文目录导读:

- 核心问题:内存溢出
- 场景一:写入百万级数据(使用 SXSSFWorkbook)
- 场景二:读取百万级数据(使用 XSSFReader + SAX 解析)
- 场景三:读取百万级
.xls文件(HSSF + RecordFactory) - 通用优化策略(写入和读取)
- 极端情况:真的需要读取/写入全量的百万行吗?
处理百万级数据的Excel报表,核心原则是:避免将全部数据加载到内存中,POI提供了两种模式:用户模式(UserModel,一次加载全部数据)和事件模式(EventModel,流式读取),对于百万级数据,必须使用SXSSF( Streaming Usermodel API,用于写) 和XSSF with SAX (Event API,用于读)。
以下是详细的处理策略和代码示例。
核心问题:内存溢出
- HSSFWorkbook (Excel 97-2003 .xls):最大行数65536,百万级数据无法存储。
- XSSFWorkbook (Excel 2007+ .xlsx):虽然支持百万行,但会将整个XML数据结构加载到内存,轻松消耗数GB内存,极易触发
OutOfMemoryError。 - SXSSFWorkbook:POI 3.8+ 提供,基于“滑动窗口”,它只保留窗口内的Row对象在内存中,超过窗口大小的行会被写入磁盘(临时文件),这是写入百万级数据的正确方式。
- XSSFReader + SAX:读取百万级数据的正确方式,不构建DOM树,而是边解析边处理。
写入百万级数据(使用 SXSSFWorkbook)
这是生成报表、导出数据的标准做法。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.FileOutputStream;
import java.util.Random;
public class LargeExcelWriter {
// 控制内存中保留的行数,防止过度刷盘影响性能
private static final int WINDOW_SIZE = 100;
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
// 1. 创建SXSSFWorkbook,指定窗口大小
// 可以开启压缩临时文件(磁盘IO换内存)
SXSSFWorkbook wb = new SXSSFWorkbook(WINDOW_SIZE);
wb.setCompressTempFiles(true); // 有助于降低磁盘占用
Sheet sheet = wb.createSheet("百万数据");
// 2. 写入表头(可复用样式)
Row headerRow = sheet.createRow(0);
String[] headers = {"ID", "姓名", "数值列", "日期"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}
// 3. 批量写入数据(模拟100万行)
int totalRows = 1_000_000;
Random random = new Random();
for (int i = 1; i <= totalRows; i++) {
Row row = sheet.createRow(i);
row.createCell(0).setCellValue(i);
row.createCell(1).setCellValue("用户_" + i);
row.createCell(2).setCellValue(random.nextDouble() * 1000);
// 性能优化:每10万行刷一次数据到磁盘(并清理JVM内存)
if (i % 100_000 == 0) {
System.out.println("已写入 " + i + " 行...");
// 强制将内存中的行写入磁盘,但不会清除工作簿引用
// 通常不需要手动调用,SXSSF会自动处理
}
}
// 4. 写入到文件(务必使用 FileOutputStream,不要用 ByteArrayOutputStream 避免内存爆炸)
try (FileOutputStream fos = new FileOutputStream("million_rows.xlsx")) {
wb.write(fos);
}
System.out.println("写入完成!耗时:" + (System.currentTimeMillis() - startTime) + "ms");
// 5. 清理临时文件
wb.dispose();
}
}
关键优化点
setCompressTempFiles(true):临时文件压缩,减少磁盘IO,但会略微增加CPU开销,通常推荐开启。- 不要使用过多的样式:SXSSF每创建一个CellStyle都会占用内存,如果样式很多,考虑使用缓存池。
- 批量提交:每写一批行(如10万行)后,可以调用
((SXSSFSheet)sheet).flushRows(0)强制刷盘,避免内存波动。
读取百万级数据(使用 XSSFReader + SAX 解析)
绝对不能使用XSSFWorkbook或WorkbookFactory创建对象。
必须使用SAX(Simple API for XML) 方式逐行读取xlsx文件,xlsx本质是一个ZIP包,里面的xml文件逐行包含数据。
完整示例代码
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
public class LargeExcelReader {
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
// 1. 打开 ZIP 包(OPCPackage)
// 注意:路径要大文件,不要使用 new FileInputStream() 包装
try (OPCPackage pkg = OPCPackage.open("million_rows.xlsx")) {
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
// 2. 获取第一个 Sheet 的输入流
InputStream sheetStream = r.getSheetsData().next();
// 3. 使用 SAX 解析
XMLReader parser = XMLReaderFactory.createXMLReader();
// 4. 核心:设置 Sheet 内容的处理器
XSSFSheetXMLHandler handler = new XSSFSheetXMLHandler(
r.getStylesTable(),
null, // 不需要读取注释
sst,
new SimpleSheetContentsHandler(), // 自定义处理器
false, // 是否格式化数字,false可以提高性能
false // 是否输出空白单元格
);
parser.setContentHandler(handler);
parser.parse(new InputSource(sheetStream));
}
System.out.println("读取完成!耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
// 自定义处理器:每个单元格的数据都会回调这里
private static class SimpleSheetContentsHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
private int currentRow = 0;
private int currentCol = -1;
private StringBuilder sb = new StringBuilder();
@Override
public void startRow(int rowNum) {
currentRow = rowNum;
currentCol = -1;
// 每开始一行,打印行号,可以做批量处理、数据库插入等
// (这里仅为演示,实际场景应写入数据库或文件)
}
@Override
public void endRow(int rowNum) {
// 一行结束,通常提交一行的数据
// 百万级数据,建议每N行批量写入数据库或文件,不要拼接字符串
sb.append("Row: ").append(rowNum).append("\n");
if (rowNum % 100000 == 0) {
System.out.println("已处理行: " + rowNum);
}
}
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
// formattedValue 已经是字符串格式(基于Excel的格式)
currentCol++;
sb.append(" Column: ")
.append(cellReference)
.append(" -> ")
.append(formattedValue)
.append("\n");
}
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
// 忽略页眉页脚
}
}
}
读取注意事项
- OPCPackage.open(File):直接打开文件,不要先读取到InputStream再传入,否则文件会自动解压到内存导致内存爆炸。
- SharedStringsTable:共享字符串表(xlsx里所有文本去重后存于此),解析完成后及时置为null帮助GC。
- 批量提交:在
endRow或每读N行后,将数据写入数据库或中间文件,不要用List<Row>收集全部数据。 - 数字格式化:如果不需要精确的数字格式,设置
truefalse来禁用格式化提升性能。
读取百万级 .xls 文件(HSSF + RecordFactory)
如果文件是旧版.xls(使用HSSF),可以使用EventUserModel。
import org.apache.poi.hssf.eventusermodel.*;
import org.apache.poi.hssf.record.*;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
public class LargeXlsReader {
public static void main(String[] args) throws Exception {
try (POIFSFileSystem fs = new POIFSFileSystem(new java.io.File("large.xls"));
InputStream is = fs.createDocumentInputStream("Workbook")) {
// 创建事件监听器
HSSFRequest req = new HSSFRequest();
req.addListenerForAllRecords(new HSSFRecordListener() {
@Override
public void processRecord(Record record) {
// 处理记录,只关注 RowRecord 和 CellRecord
if (record instanceof RowRecord) {
RowRecord row = (RowRecord) record;
// 处理行
} else if (record instanceof NumberRecord) {
NumberRecord cell = (NumberRecord) record;
// 处理数字单元格
} else if (record instanceof LabelRecord || record instanceof LabelSSTRecord) {
// 处理字符串单元格
}
// 注意:SSTRecord需要用HSSFWorkbook读取,但我们可以延迟加载
}
});
// 解析
new HSSFEventFactory().processWorkbookEvents(req, is);
}
}
}
通用优化策略(写入和读取)
| 策略 | 说明 | 影响 |
|---|---|---|
| 增加JVM堆内存 | -Xms4g -Xmx8g -XX:+UseG1GC |
简单有效,但仍是治标 |
| 减少单元格样式 | 复用Style、Font,不要每行都新创建 | 降低内存损耗 |
| 分批写入数据库 | 每1000~5000行组装一个批量Insert | 减少事务开销、快写 |
| 禁用自动列宽 | sheet.autoSizeColumn 很耗性能,百万行不要用 |
避免逐行扫描 |
| 使用列式存储 | 如果数据是列宽固定、数值类型多,可以考虑CSV或Parquet | 大数据处理首选 |
| 考虑CSV | 如果不需要公式/合并单元格等Excel特性,CSV处理起来快10倍以上 | 读取简单,写入用OpenCSV |
| 多线程写入 | 写出不同Sheet到不同文件,最终合并 | 对磁盘IO有压力 |
| 使用临时文件 | SXSSF默认写到临时目录,确保磁盘有足够空间 | 避免内存不够转为磁盘用尽 |
极端情况:真的需要读取/写入全量的百万行吗?
- 分页导出:用户很少一次导出全部百万行,应当使用游标或分页,一次提取部分行。
- 直接生成CSV:如果对方系统也支持CSV,生成CSV可以绕开POI,直接使用
BufferedWriter写文件,速度极快。 - 使用更高级的库:如 EasyExcel(阿里巴巴),底层也是SAX解析,但封装更友好、内存优化更好,对于读百万级数据,建议使用EasyExcel替代原始POI Event API。
- 写入:首选SXSSF(POI),配合窗口大小和压缩临时文件。
- 读取:避免XSSFWorkbook,必须使用XSSFReader + SAX(或直接使用EasyExcel)。
- 终极方案:如果是内部系统,考虑使用CSV或数据库直连导出,避免Excel处理瓶颈。