背景
在做商品EXCEL的時候,線上發現了Full GC,排查得知是商家搞了一個巨大的excel,單商品釋出接口平均耗時400ms(調用sell耗時200ms左右,系統自身處理商品同步耗時150ms左右),對于3000個商品的釋出,耗時在20min左右,這20min内該excel的記憶體一直未能釋放。

第一時間想到的是POI真坑,真吃記憶體。 事情發生了就想着怎麼處理,
- 止血 線上機器分批重新開機,
- 馬上加一個excel行數的限制然後釋出 線上半個小時左右就沒有任何問題了。
思考
為什麼poi這麼吃記憶體,poi這麼老了,肯定有人踩過這個坑,撸起袖子,搜poi full gc. 很多文檔将的都太粗糙了,本質沒有說透
原因
- excel本質上是xml檔案的集合體。從office 2007起開始使用xml來存檔和資料交換: https://zh.wikipedia.org/wiki/Office_Open_XML
- poi預設是使用dom方式解析excel,是以檔案中String的數量越多,其dom樹越大。
解法
由于excel商品釋出不需要動态的更改excel中的資料,是以并不強依賴dom解析,直接換成sax來解析excel就行
Action
poi中sax用法
/**
* @author zhengqiang.zq
* @date 2018/05/04 ,參考連結:https://poi.apache.org/spreadsheet/how-to.html#sxssf
*/
public class MyEventUserModel {
public static ThreadLocal<List<ParsedRow>> local = new ThreadLocal<>();
public void processOneSheet(String filename) throws Exception {
OPCPackage pkg = OPCPackage.open(filename);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst);
//從workbook.xml.res 中擷取所有需要解析的xml檔案,rid1 就是第一個sheet,其target就是該sheet所在的相對路徑
//<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
//<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
// <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
// <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
// <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
// <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
// <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>
//</Relationships>
//
InputStream sheet2 = r.getSheet("rId1");
InputSource sheetSource = new InputSource(sheet2);
parser.parse(sheetSource);
sheet2.close();
}
public XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException {
XMLReader parser =
XMLReaderFactory.createXMLReader(
"org.apache.xerces.parsers.SAXParser"
);
ContentHandler handler = new SheetHandler(sst);
parser.setContentHandler(handler);
return parser;
}
/**
* See org.xml.sax.helpers.DefaultHandler javadocs
*/
private class SheetHandler extends DefaultHandler {
/**
* excel 常量資料對象,對應的就是sharedStrings.xml檔案中的内容,類似excel中的常量池
*/
private SharedStringsTable sst;
/**
* 目前處理的文本值
*/
private String lastContents;
/**
* 下一個文本是不是String類型
*/
private boolean nextIsString;
/**
* 目前單元格的索引值,從0開始,0:第一列
*/
private Short index;
/**
* 自定義資料類型,存儲被解析的每一行原始資料
*/
List<ParsedRow> sheetData = Lists.newArrayList();
ParsedRow currentRow = new ParsedRow();
private SheetHandler(SharedStringsTable sst) {
this.sst = sst;
}
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
//第一行
if (name.equals("row")) {
currentRow.setRowNum(new Long(attributes.getValue("r")));
sheetData.add(currentRow);
}
//c => cell 一個單元格,
if (name.equals("c")) {
//r屬性表示單元格位置,例如A2,C3
String coordinate = attributes.getValue("r");
CellReference cellReference = new CellReference(coordinate);
//根據r屬性擷取其列下标,從0開始
index = cellReference.getCol();
//t:屬性代表單元格類型
String cellType = attributes.getValue("t");
if (cellType != null && cellType.equals("s")) {
//t="s"表示是改單元格是字元串,那麼該單元格的實際值值需要去SharedStringsTable中取
nextIsString = true;
} else {
nextIsString = false;
}
}
// Clear contents cache
lastContents = "";
}
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
if (nextIsString) {
int idx = Integer.parseInt(lastContents);
//從SharedStringsTable中取目前單元格的實際值
lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
nextIsString = false;
}
// v => contents of a cell
// Output after we've seen the string contents
if (name.equals("v")) {
//不管是不是數字還是文本值
currentRow.getData().put(index, lastContents);
}
if (name.equals("row")) {
currentRow = new ParsedRow();
}
}
@Override
public void endDocument() throws SAXException {
local.set(sheetData);
}
/**
* 通知一個元素中的字元,是否處理由自己決定,比如 <v>1</v>,
*
* @param ch The characters. 整個sheet.xml的char[]數組表示
* @param start The start position in the character array. 本次處理的元素值的的開始位置
* @param length The number of characters to use from the ,元素長度
* character array.
* @throws SAXException Any SAX exception, possibly
* wrapping another exception.
* @see ContentHandler#characters
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
//對于lastContents是String類型來說,lastContent存放的是其在SharedStringsTable中的索引,
// 對于是數字類型來說,lastContents存放就是該數字的字元串表示
lastContents += new String(ch, start, length);
}
}
public static void main(String[] args) throws Exception {
String fileName = "/Users/thinerzq/alltest/excel/test_big_3300_diffrent_row.xlsx";
MyEventUserModel example = new MyEventUserModel();
Stopwatch stopwatch = new Stopwatch();
stopwatch.start();
example.processOneSheet(fileName);
System.out.println("-----------------finish, " + stopwatch.toString());
System.out.println(local.get());
Thread.sleep(100000 * 1000);
}
}
性能對比
dom 3455行
解析時間
-----------------finish, 6.987 s
記憶體消耗
jmap -histo:live 2646
thinerzq@thinerzq-2:~$ jmap -histo:live 2646
num #instances #bytes class name
----------------------------------------------
1: 2574454 247147584 org.apache.xmlbeans.impl.store.Xobj$AttrXobj
2: 1332126 127884096 org.apache.xmlbeans.impl.store.Xobj$ElementXobj
3: 1265264 50610560 java.util.TreeMap$Entry
4: 778421 48667664 [C
5: 636 37006672 [B
6: 611910 29371680 java.util.TreeMap
7: 611886 24475440 org.apache.xmlbeans.impl.values.XmlUnsignedIntImpl
8: 653334 20906688 org.apache.poi.xssf.usermodel.XSSFCell
9: 653334 20906688 org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STCellRefImpl
10: 775269 18606456 java.lang.String
11: 653334 15680016 org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellImpl
12: 611866 14684784 org.apache.poi.xssf.usermodel.XSSFRow
13: 611866 14684784 org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRowImpl
14: 622210 9955360 java.lang.Integer
15: 55239 1767648 org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STXstringImpl
16: 34552 1105664 org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STCellTypeImpl
17: 18776 600832 java.util.HashMap$Node
18: 10328 247872 org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRstImpl
….
726: 1 16 sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total 11909576 686173768=81.8MB
sax 3455行
-----------------finish, 2.427 s
thinerzq@thinerzq-2:~$ jmap -histo:live 2711
num #instances #bytes class name
----------------------------------------------
1: 612060 29378880 java.util.HashMap
2: 611866 14684784 com.zq.poi.ParsedRow
3: 611866 14684784 java.lang.Long
4: 1298 3342120 [Ljava.lang.Object;
5: 60140 3149488 [C
6: 51946 1662272 java.util.HashMap$Node
7: 60096 1442304 java.lang.String
8: 3610 578024 [Ljava.util.HashMap$Node;
9: 617 288960 [B
10: 1626 186544 java.lang.Class
11: 975 173176 [I
12: 2911 116440 java.util.LinkedHashMap$Entry
13: 2648 63552 javax.xml.namespace.QName
14: 1811 57952 java.util.concurrent.ConcurrentHashMap$Node
15: 2242 53808 org.apache.xmlbeans.SchemaType$Ref
16: 423 27072 java.net.URL
17: 1619 25904 java.lang.Object
18: 290 20496 [Ljava.lang.String;
531: 1 16 sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total 2036715 70309432 =8.4MB
總覽
解析類型 | 資料量 | 記憶體占用 | |
---|---|---|---|
dom | 3455行不同資料 | 6.587 s | 81.8MB |
sax | 2.427 s | 8.4MB | |
10000行資料,2/3重複 | 6.748s | 100.4M | |
2.827s | 9.4MB |
可以看到使用sax解析之後記憶體下降了近10倍之多,再也不用擔心full gc了。由于其常量池的緣故,excel檔案大小和行數是否有重複的單元格有關系。
其他
參考連結
檢視excel的xml檔案
将excel檔案的字尾名改為.zip 然後解壓縮裡面就全部都是xml檔案了。