题目:POI 导致内存溢出排查
难度:high
标签:practice/debug, jvm/oom, java/poi, difficulty/high
评分:⭐⭐☆☆☆(2/5)
POI导出大批量数据导致OOM,核心原因是单次将整个文件都加载到内存中然后做解析导致的,这块在排查时可以根据oom的错误日志,查看具体oom产生原因,oom的异常信息,对应线程栈信息,应该能看到对应证据调用的节点信息等;
针对这种问题,可以调整导出逻辑修改为流式导出,不要一次性进行加载,而是分配进行导出读取到内存中
优点:抓住了核心矛盾——POI 默认的 usermodel 模式是全量 DOM,整个工作簿常驻内存;也给出了正确方向:改流式。OOM 日志 → 线程栈 → 调用点的思路没错。
不足:① 没点出 POI 的具体对象模型:XSSFWorkbook 持有 XSSFSheet → XSSFRow → XSSFCell,每个 cell 都是对象(含样式、共享字符串引用),20 万行 × 20 列 = 400 万个对象,加上 SharedStringsTable 和 StylesTable,几个 GB 是常态;② 没区分写与读的流式方案:写用 SXSSFWorkbook(滑动窗口 + 刷盘临时文件),读用 XSSFReader + SAX 事件模型(EasyExcel 底层就是它),"流式"两字太笼统;③ 没提排查证据的具体形态:堆转储里应看到 XSSFCell/CT* 的实例数霸榜,或有大量 POI 临时文件;④ 没提 SXSSF 的隐藏坑:windowSize 设置不当仍然占内存、临时文件要 dispose() 否则磁盘打满、autoSizeColumn 会额外缓存列宽、样式对象要复用而不是每个 cell 都 createCellStyle。
补充:还有一个容易忽略的点——POI 处理 xlsx 底层是 zip 流,java.util.zip 的 Inflater/Deflater 会占直接内存,所以有时报的不是 Java heap space 而是 Direct buffer memory 或容器 OOMKilled,堆快照里看不出问题,要用 NMT 看。这类问题的根治往往不在代码,而是把"同步导出 20 万行"改成"异步任务 + 分片生成 + 文件存对象存储 + 通知下载"。
核心考点定位:考察对 POI 内存模型的理解深度,以及能否把"OOM"从现象推到"对象模型 + 流式方案 + 架构改造"三层。
知识点拆解:
XSSFWorkbook 把整个 xlsx 解析成内存对象树,Sheet → Row → Cell 逐层持有;每个 XSSFCell 除了值还持有样式索引、数据类型等字段。20 万行 × 20 列就是 400 万个 cell 对象,加上 SharedStringsTable(所有字符串去重后常驻)与 StylesTable,堆占用轻松上 GB。XSSFWorkbook,只保留最近 windowSize 行在内存,超出部分刷到磁盘临时文件,最后统一打包成 zip。内存与 windowSize × 列数 成正比,而不是总行数。注意:windowSize 默认 100,设太大等于没优化;临时文件必须 dispose();autoSizeColumn 在 SXSSF 下不支持或很贵。XSSFReader + SheetContentsHandler(SAX)逐行解析,内存占用与文件大小无关,只与单行大小相关。EasyExcel 的读就是这套模型的封装;写侧 EasyExcel 也基于 SXSSF。createCellStyle() 每次调用都会新增样式,xlsx 样式上限约 64000,超了报错,且样式表常驻内存。正确做法是复用少量 CellStyle 对象。这是 POI 导出最隐蔽的内存放大器。java.io.tmpdir 生成临时文件,磁盘满会直接导致导出失败;容器里 tmp 往往是 emptyDir,容量小,必须显式指定目录并做清理。Inflater/Deflater,占用直接内存或 native 内存。报 Direct buffer memory 或容器 OOMKilled 时,堆快照正常,要看 NMT 与 -XX:MaxDirectMemorySize。按「底层原理 → 设计权衡 → 工程实践」递进排列。
考察方向:
延伸递进面试题:
【一句话结论】
面试官您好,POI 导出 OOM 的根因是 usermodel 把整个工作簿解析成内存对象树,行数一多就是几百万个 cell 对象。
【三点展开】
第一,先看清对象模型。XSSFWorkbook 下面挂着 Sheet、Row、Cell,每个 Cell 都是独立对象,还带样式索引;共享字符串表和样式表也是全量常驻。20 万行乘 20 列就是 400 万个对象,堆涨到几个 G 很正常。堆转储里能看到 XSSFCell 的实例数霸榜,这是最直接的证据。
第二,改造分读写两条路。写用 SXSSFWorkbook,它只保留 windowSize 行在内存,其余刷到磁盘临时文件,最后打包成 zip,内存跟总行数脱钩;读用 XSSFReader 加 SAX 事件模型,EasyExcel 就是这套封装。要注意 SXSSF 的 windowSize 别设太大、临时文件用完要 dispose、样式对象必须复用,不然内存换个地方继续涨。
第三,架构上再兜一层。真正的大数据量导出不该同步返回,改成异步任务加分片生成,文件放对象存储再通知下载,同时对单次导出量设上限。
【一句边界】
当然,如果报的是 Direct buffer memory 而不是 Java heap space,那是 zip 压缩用的直接内存,堆快照看不出来,要用 NMT 查,光改 SXSSF 不一定能解决。