解决 Java OOM 内存溢出:利用 MAT 分析心理档案海量 Excel 导出引起的堆爆炸

直击7.5GB内存暴涨的导出死亡现场。抽丝剥茧MAT分析Heap Dump,废弃POI,换装EasyExcel极客滑动窗口与物理刷盘,将堆消耗极其狂暴地压缩至50MB。

在市级心理干预平台后台,常会有导出大量数据的需求,例如将数十万名学生的心理测评原始数据导出为 Excel 文件。

早期版本中,系统采用了比较常规的处理方式:通过 MyBatis 一次性查询出数十万条数据放入 List 集合,随后调用 Apache POI 库生成 Excel 文件。这种方案在上线后,当用户触发全量数据导出时,服务器出现了 java.lang.OutOfMemoryError: Java heap space 错误,导致整个微服务节点内存溢出并重启,影响了正在进行测试的用户的正常访问。

面对这种典型的 OOM 问题,仅仅增加物理内存治标不治本。我们需要通过专业的工具分析堆内存的实际消耗情况,找出引发内存泄漏或激增的源头。

提取 Heap Dump 并进行 MAT 分析

为了能够事后排查,我们在 JVM 启动参数中预先配置了 OOM 时自动导出堆快照的选项:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/log/heapdump.hprof

当系统再次发生内存溢出时,JVM 自动生成了一份完整的堆内存转储文件。我们获取到 heapdump.hprof 文件后,引入了 Eclipse MAT(Memory Analyzer Tool)进行深度分析。

打开 MAT 的 Leak Suspects 报告,可以清晰地看到某个线程占用了绝大部分的堆内存资源。通过进一步查看 Dominator Tree(支配树视图)并展开引用链,发现内存并非单纯被数据实体列表消耗,而是被大量的 org.apache.poi.xssf.usermodel.XSSFRowXSSFCell 对象占据。

传统 Apache POI 在生成 Excel 时,会将所有的行、列、单元格及其样式作为完整的 Java 对象保留在内存中。对于数十万行的数据量,这种机制会产生数以千万计的小对象,导致老年代内存被迅速撑爆。

滑动窗口与物理刷盘机制

定位到内存消耗过高的原因后,我们将底层导出组件替换为了阿里开源的 EasyExcel(其底层封装了 POI 的 SXSSFWorkbook)。

与传统的 XSSFWorkbook 不同,EasyExcel 采用了滑动窗口与物理刷盘的机制。在生成大规模 Excel 文件时,它设定了一个阈值窗口(例如 1000 行)。当在内存中拼装满 1000 行数据后,组件会自动将这部分数据 flush 到硬盘上的临时 XML 文件中,并释放内存中这批对象的引用,交由 GC 回收。

同时,配合数据库层的流式查询(Stream Query),服务端可以分批次从 MySQL 中读取数据,再通过 EasyExcel 分批写入磁盘,使得整个导出过程的内存曲线保持平稳。

在涉及海量数据处理的 B 端场景下,将所有数据一股脑加载至内存的做法往往会带来系统可用性风险。橙星云技术团队在解决此次 OOM 故障后,将流式查询和分批刷盘规范化,使得系统在处理百万级数据导出时,内存占用依然可以维持在几十 MB 的安全水位,提升了整体架构的健壮性。

Leave a Reply

Your email address will not be published. Required fields are marked *