快手二面:敢不敢說(shuō)說(shuō)為啥POI會(huì)導(dǎo)致內(nèi)存溢出?
Apache POI,是一個(gè)非常流行的文檔處理工具,通常大家會(huì)選擇用它來(lái)處理Excel文件。但是在實(shí)際使用的時(shí)候,經(jīng)常會(huì)遇到內(nèi)存溢出的情況,那么,為啥他會(huì)導(dǎo)致內(nèi)存溢出呢?
Excel并沒(méi)看到的那么小
我們通常見到的xlsx文件,其實(shí)是一個(gè)個(gè)壓縮文件。它們把若干個(gè)XML格式的純文本文件壓縮在一起,Excel就是讀取這些壓縮文件的信息,最后展現(xiàn)出一個(gè)完全圖形化的電子表格。
所以,如果我們把xlsx文件的后綴更改為.zip或.rar,再進(jìn)行解壓縮,就能提取出構(gòu)成Excel的核心源碼文件。解壓后會(huì)發(fā)現(xiàn)解壓后的文件中有3個(gè)文件夾和1個(gè)XML格式文件:
圖片
_rels 文件夾 看里面數(shù)據(jù)像是一些基礎(chǔ)的配置信息,比如 workbook 文件的位置等信息,一般不會(huì)去動(dòng)它.
docProps 文件夾下重要的文件是一個(gè) app.xml,這里面主要存放了 sheet 的信息,如果想添加或編輯 sheet 需要改這個(gè)文件.其他文件都是一些基礎(chǔ)信息的數(shù)據(jù),比如文件所有者,創(chuàng)建時(shí)間等.
xl 文件夾是最重要的一個(gè)文件夾,里面存放了 Sheet 中的數(shù)據(jù),行和列的格式,單元格的格式,sheet 的配置信息等等信息.
所以,實(shí)際上我們處理的xlsx文件實(shí)際上是一個(gè)經(jīng)過(guò)高度壓縮的文件格式,背后是有好多文件支持的。所以,我們看到的一個(gè)文件可能只有2M,但是實(shí)際上這個(gè)文件未壓縮情況下可能要比這大得多。
圖片
也就是說(shuō),POI在處理的時(shí)候,處理的實(shí)際上并不只是我們看到的文件大小,實(shí)際上他的大小大好幾倍。(本文節(jié)選自我的《java面試寶典》)
這是為什么明明我們處理的文件只有100多兆,但是實(shí)際卻可能占用1G內(nèi)存的其中一個(gè)原因。當(dāng)然這只是其中一個(gè)原因,還有一個(gè)原因,我們就需要深入到POI的源碼中來(lái)看了。
POI溢出原理
我們拿POI的文件讀取來(lái)舉例,一般來(lái)說(shuō)文件讀取出現(xiàn)內(nèi)存溢出的情況更多一些。以下是一個(gè)POI文件導(dǎo)出的代碼示例:
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))) {
// 創(chuàng)建工作簿對(duì)象
Workbook workbook = new XSSFWorkbook(fileInputStream);
// 獲取第一個(gè)工作表
Sheet sheet = workbook.getSheetAt(0);
// 遍歷所有行
for (Row row : sheet) {
// 遍歷所有單元格
for (Cell cell : row) {
// 根據(jù)不同數(shù)據(jù)類型處理數(shù)據(jù)
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);
}
}
}
這里面用到了一個(gè)關(guān)鍵的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);
}
}
最終會(huì)調(diào)用到OPCPackage.open方法,看看這個(gè)方法是咋實(shí)現(xiàn)的:
/**
* 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;
}
這行代碼的注釋中說(shuō)了:這個(gè)方法會(huì)把整個(gè)壓縮文件都加載到內(nèi)存中。也就是把整個(gè) Excel 文檔加載到內(nèi)存中,可想而知,這在處理大型文件時(shí)是肯定會(huì)導(dǎo)致導(dǎo)致內(nèi)存溢出的。(本文節(jié)選自我的《java面試寶典》,里面有800多道面試常考題目)
也就是說(shuō)我們使用的XSSFWorkbook(包括HSSFWorkbook也同理)在處理Excel的過(guò)程中會(huì)將整個(gè)Excel都加載到內(nèi)存中,在文件比較大的時(shí)候就會(huì)導(dǎo)致內(nèi)存溢出。
如何解決溢出問(wèn)題?
在POI中,提供了SXSSFWorkbook,通過(guò)將部分?jǐn)?shù)據(jù)寫入磁盤上的臨時(shí)文件來(lái)減少內(nèi)存占用。但是SXSSFWorkbook只能用于文件寫入,但是文件讀取還是不行的,就像我們前面分析過(guò)的,Excel的文件讀取還是會(huì)存在內(nèi)存溢出的問(wèn)題的。
那如果要解決這個(gè)問(wèn)題,可以考慮使用EasyExcel?。ū疚墓?jié)選自我的《java面試寶典》,里面有800多道面試??碱}目)
關(guān)于使用XSSFWorkbook和EasyExcel的文件讀取,我這里也做了個(gè)內(nèi)存占用的對(duì)比,讀取一個(gè)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))) {
// 創(chuàng)建工作簿對(duì)象
Workbook workbook = new XSSFWorkbook(fileInputStream);
// 獲取第一個(gè)工作表
Sheet sheet = workbook.getSheetAt(0);
// 遍歷所有行
for (Row row : sheet) {
// 遍歷所有單元格
for (Cell cell : row) {
// 根據(jù)不同數(shù)據(jù)類型處理數(shù)據(jù)
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查看內(nèi)存占用情況:
圖片
占用內(nèi)存在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();
}
}
// 監(jiān)聽器,用于處理讀取到的數(shù)據(jù)
class PrintDataListener implements ReadListener<Object> {
@Override
public void invoke(Object data, AnalysisContext context) {
// 處理每一行的數(shù)據(jù)
System.out.println(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 所有數(shù)據(jù)解析完成后的操作
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 處理讀取過(guò)程中的異常
}
}
同樣使用Arthas查看內(nèi)存占用情況:
圖片
內(nèi)存占用只有不到100M。