快手二面:敢不敢說說為啥POI會導致內存溢出?
Apache POI,是一個非常流行的文檔處理工具,通常大家會選擇用它來處理Excel文件。但是在實際使用的時候,經常會遇到內存溢出的情況,那么,為啥他會導致內存溢出呢?
Excel并沒看到的那么小
我們通常見到的xlsx文件,其實是一個個壓縮文件。它們把若干個XML格式的純文本文件壓縮在一起,Excel就是讀取這些壓縮文件的信息,最后展現出一個完全圖形化的電子表格。
所以,如果我們把xlsx文件的后綴更改為.zip或.rar,再進行解壓縮,就能提取出構成Excel的核心源碼文件。解壓后會發現解壓后的文件中有3個文件夾和1個XML格式文件:
圖片
_rels 文件夾 看里面數據像是一些基礎的配置信息,比如 workbook 文件的位置等信息,一般不會去動它.
docProps 文件夾下重要的文件是一個 app.xml,這里面主要存放了 sheet 的信息,如果想添加或編輯 sheet 需要改這個文件.其他文件都是一些基礎信息的數據,比如文件所有者,創建時間等.
xl 文件夾是最重要的一個文件夾,里面存放了 Sheet 中的數據,行和列的格式,單元格的格式,sheet 的配置信息等等信息.
所以,實際上我們處理的xlsx文件實際上是一個經過高度壓縮的文件格式,背后是有好多文件支持的。所以,我們看到的一個文件可能只有2M,但是實際上這個文件未壓縮情況下可能要比這大得多。
圖片
也就是說,POI在處理的時候,處理的實際上并不只是我們看到的文件大小,實際上他的大小大好幾倍。(本文節選自我的《java面試寶典》)
這是為什么明明我們處理的文件只有100多兆,但是實際卻可能占用1G內存的其中一個原因。當然這只是其中一個原因,還有一個原因,我們就需要深入到POI的源碼中來看了。
POI溢出原理
我們拿POI的文件讀取來舉例,一般來說文件讀取出現內存溢出的情況更多一些。以下是一個POI文件導出的代碼示例:
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class ExcelReadTest {
public static void main(String[] args) {
// 指定要讀取的文件路徑
String filename = "example.xlsx";
try (FileInputStream fileInputStream = new FileInputStream(new File(filename))) {
// 創建工作簿對象
Workbook workbook = new XSSFWorkbook(fileInputStream);
// 獲取第一個工作表
Sheet sheet = workbook.getSheetAt(0);
// 遍歷所有行
for (Row row : sheet) {
// 遍歷所有單元格
for (Cell cell : row) {
// 根據不同數據類型處理數據
switch (cell.getCellType()) {
case STRING:
System.out.print(cell.getStringCellValue() + "\t");
break;
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
System.out.print(cell.getDateCellValue() + "\t");
} else {
System.out.print(cell.getNumericCellValue() + "\t");
}
break;
case BOOLEAN:
System.out.print(cell.getBooleanCellValue() + "\t");
break;
case FORMULA:
System.out.print(cell.getCellFormula() + "\t");
break;
default:
System.out.print(" ");
}
}
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
這里面用到了一個關鍵的XSSFWorkbook類:
public XSSFWorkbook(InputStream is) throws IOException {
this(PackageHelper.open(is));
}
public static OPCPackage open(InputStream is) throws IOException {
try {
return OPCPackage.open(is);
} catch (InvalidFormatException e){
throw new POIXMLException(e);
}
}
最終會調用到OPCPackage.open方法,看看這個方法是咋實現的:
/**
* Open a package.
*
* Note - uses quite a bit more memory than {@link #open(String)}, which
* doesn't need to hold the whole zip file in memory, and can take advantage
* of native methods
*
* @param in
* The InputStream to read the package from
* @return A PackageBase object
*
* @throws InvalidFormatException
* Throws if the specified file exist and is not valid.
* @throws IOException If reading the stream fails
*/
public static OPCPackage open(InputStream in) throws InvalidFormatException,
IOException {
OPCPackage pack = new ZipPackage(in, PackageAccess.READ_WRITE);
try {
if (pack.partList == null) {
pack.getParts();
}
} catch (InvalidFormatException | RuntimeException e) {
IOUtils.closeQuietly(pack);
throw e;
}
return pack;
}
這行代碼的注釋中說了:這個方法會把整個壓縮文件都加載到內存中。也就是把整個 Excel 文檔加載到內存中,可想而知,這在處理大型文件時是肯定會導致導致內存溢出的。(本文節選自我的《java面試寶典》,里面有800多道面試常考題目)
也就是說我們使用的XSSFWorkbook(包括HSSFWorkbook也同理)在處理Excel的過程中會將整個Excel都加載到內存中,在文件比較大的時候就會導致內存溢出。
如何解決溢出問題?
在POI中,提供了SXSSFWorkbook,通過將部分數據寫入磁盤上的臨時文件來減少內存占用。但是SXSSFWorkbook只能用于文件寫入,但是文件讀取還是不行的,就像我們前面分析過的,Excel的文件讀取還是會存在內存溢出的問題的。
那如果要解決這個問題,可以考慮使用EasyExcel!(本文節選自我的《java面試寶典》,里面有800多道面試常考題目)
關于使用XSSFWorkbook和EasyExcel的文件讀取,我這里也做了個內存占用的對比,讀取一個27.3?MB的文件:
package excel.read;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class XSSFExcelReadTest {
public static void main(String[] args) {
// 指定要讀取的文件路徑
String filename = "example.xlsx";
try (FileInputStream fileInputStream = new FileInputStream(new File(filename))) {
// 創建工作簿對象
Workbook workbook = new XSSFWorkbook(fileInputStream);
// 獲取第一個工作表
Sheet sheet = workbook.getSheetAt(0);
// 遍歷所有行
for (Row row : sheet) {
// 遍歷所有單元格
for (Cell cell : row) {
// 根據不同數據類型處理數據
switch (cell.getCellType()) {
case STRING:
System.out.print(cell.getStringCellValue() + "\t");
break;
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
System.out.print(cell.getDateCellValue() + "\t");
} else {
System.out.print(cell.getNumericCellValue() + "\t");
}
break;
case BOOLEAN:
System.out.print(cell.getBooleanCellValue() + "\t");
break;
case FORMULA:
System.out.print(cell.getCellFormula() + "\t");
break;
default:
System.out.print(" ");
}
}
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用Arthas查看內存占用情況:
圖片
占用內存在1000+M。
改成使用EasyExcel同樣讀取同一份文件:
package excel.read;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
public class EasyExcelReadTest {
public static void main(String[] args) {
// 指定要讀取的文件路徑
String filename = "example.xlsx";
EasyExcel.read(filename, new PrintDataListener()).sheet().doRead();
}
}
// 監聽器,用于處理讀取到的數據
class PrintDataListener implements ReadListener<Object> {
@Override
public void invoke(Object data, AnalysisContext context) {
// 處理每一行的數據
System.out.println(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 所有數據解析完成后的操作
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 處理讀取過程中的異常
}
}
同樣使用Arthas查看內存占用情況:
圖片
內存占用只有不到100M。