背景
在做商品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
内存消耗
jmap -histo:live 2646
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 | 3455行不同数据 | 2.427 s | 8.4MB |
dom | 10000行数据,2/3重复 | 6.748s | 100.4M |
sax | 10000行数据,2/3重复 | 2.827s | 9.4MB |
可以看到使用sax解析之后内存下降了近10倍之多,再也不用担心full gc了。由于其常量池的缘故,excel文件大小和行数是否有重复的单元格有关系。
其他
参考链接
查看excel的xml文件
将excel文件的后缀名改为.zip 然后解压缩里面就全部都是xml文件了。