咸鱼开发修炼之路

POI导出Excel导致内存溢出解决

记录一次由于大批量Excel导出导致的内存溢出事故。

pasted-15.png

发现和处理过程

  1. 下午17:30左右运维反馈系统内存溢出
  2. 联系运维重启服务器恢复正常
  3. 运维将112服务器的-XX:PermSize=128M -XX:MaxPermSize=256M调整为-XX:PermSize=256M -XX:MaxPermSize=512M;

事故原因分析

  1. 根据dump文件发现17:35分左右系统产生大量的HSSFCell对象,初步判断是由于导出或者导入操作引起内存溢出

    pasted-5.png
  2. 搜索日志发现下午17:35分左右有人在操作大批量数据导出,大概30w条

  3. 定位到对应代码,发现使用HSSFCell对象进行导出操作,网上搜索发现HSSFWorkbook类是将所有生成的单元格都读入到内存,所以会new所有的HSSFCell对象,导致内存溢出pasted-7.png

问题解决

更换SXSSFWorkbook类进行Excel导出操作,该类在只在内存中保存特定行数,其余的行被写入到硬盘上。比如设置在内存中保留100行数据,当处理101行时,第1行的数据会写入到硬盘,这样在内存中SXSSFCell对象数量是 100乘以每一行的单元格数,所以java内存消耗微乎其微。
pasted-13.png

改动后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public void export(List<VirtualAcctChangeDetailDTO> vacdList, UpopVacdQuery query,HttpServletRequest request, HttpServletResponse response) {
response.setContentType("application/vnd.ms-excel");
OutputStream fout = null;
int nextRow = 0;
SXSSFWorkbook wb = null;
try {
wb = new SXSSFWorkbook();
String codedFileName = "XXXXX变动明细文件";
if (request.getHeader("User-Agent").toLowerCase().indexOf("firefox") > 0) {
codedFileName = new String(codedFileName.getBytes("UTF-8"), "ISO8859-1");
} else {
codedFileName = java.net.URLEncoder.encode(codedFileName, "utf-8");
}
response.setHeader("content-disposition", "attachment;filename=" + codedFileName + ".xlsx");
SXSSFSheet sheet = wb.createSheet(DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"));
CellStyle titleCellStyle = ExcelBase.getTitleCellStyle(wb);
CellStyle defaultCellStyle = ExcelBase.getDefaultCellStyle(wb);
defaultCellStyle.setAlignment(HSSFCellStyle.ALIGN_LEFT);
//第一行
String row1 = "记录起始日期:" + query.getStartDateStr() + " 记录终止日期:" + query.getEndDateStr();
ExcelBase.addRow(sheet, nextRow++, 0, Lists.newArrayList(row1), defaultCellStyle);
//第二行
String row2 = "总笔数:" + vacdList.size() + " 金额单位:元";
ExcelBase.addRow(sheet, nextRow++, 0, Lists.newArrayList(row2), defaultCellStyle);
//合并单元格
List<ExcelBase.Region> regions = Lists.newArrayList(new ExcelBase.Region(0, 0, 0, 15), new ExcelBase.Region(1, 1, 0, 15));
ExcelBase.mergeRegion(sheet, regions);
//第三行:标题
List<String> aggrTitles = Lists.newArrayList("户名", "借贷标识", "金额", "记账余额", "记账日期", "交易类型", "交易流水号", "交易日期", "对方账号", "对方账户名称", "对方行号", "付款方账户名称(头寸)", "商户代码", "商户名称", "附言", "退汇原因");
ExcelBase.addRow(sheet, nextRow++, 0, aggrTitles, titleCellStyle);
for (VirtualAcctChangeDetailDTO vacd : vacdList) {
List<Object> contentRow = new ArrayList<Object>();
contentRow.add(vacd.getAcctName());
contentRow.add(vacd.getDcFlag().getMsg());
contentRow.add(vacd.getTxnAmt());
contentRow.add(vacd.getAcctBal());
Date acctDate = vacd.getAcctDate();
String acctDateStr = acctDate == null ? "" : DateFormatUtils.format(acctDate, "yyyyMMdd");
contentRow.add(acctDateStr);
contentRow.add(vacd.getTxnType().getMsg());
contentRow.add(vacd.getTxnNo());
Date txnDate = vacd.getTxnDate();
String txnDateStr = txnDate == null ? "" : DateFormatUtils.format(txnDate, "yyyyMMdd");
contentRow.add(txnDateStr);
contentRow.add(vacd.getOthAcctNo());
contentRow.add(vacd.getOthAcctName());
contentRow.add(vacd.getOthBankNo());
UpopAccountTypeEnum acctType = UpopAccountTypeEnum.parse(vacd.getInsSeq());
contentRow.add(acctType != null ? acctType.getMessage() : "");
contentRow.add(vacd.getMerId());
contentRow.add(vacd.getMerName());
contentRow.add(vacd.getRemark());
contentRow.add(vacd.getRtnReason());
ExcelBase.addRow(sheet, nextRow++, 0, contentRow, defaultCellStyle);
}
fout = response.getOutputStream();
wb.write(fout);
} catch (Exception e) {
logger.error("生成XXX变动明细文件", e);
}
finally { if (wb != null) {
try {
wb.close();
} catch (Exception e) {
logger.error("close Workbook 失败", e);
}
}
if (fout != null) {
try {
fout.close();
} catch (IOException e) {
logger.error("close response output stream 失败", e);
}
}
}
}