在市级心理干预平台后台,常会有导出大量数据的需求,例如将数十万名学生的心理测评原始数据导出为 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.XSSFRow 和 XSSFCell 对象占据。
传统 Apache POI 在生成 Excel 时,会将所有的行、列、单元格及其样式作为完整的 Java 对象保留在内存中。对于数十万行的数据量,这种机制会产生数以千万计的小对象,导致老年代内存被迅速撑爆。
滑动窗口与物理刷盘机制
定位到内存消耗过高的原因后,我们将底层导出组件替换为了阿里开源的 EasyExcel(其底层封装了 POI 的 SXSSFWorkbook)。
与传统的 XSSFWorkbook 不同,EasyExcel 采用了滑动窗口与物理刷盘的机制。在生成大规模 Excel 文件时,它设定了一个阈值窗口(例如 1000 行)。当在内存中拼装满 1000 行数据后,组件会自动将这部分数据 flush 到硬盘上的临时 XML 文件中,并释放内存中这批对象的引用,交由 GC 回收。
同时,配合数据库层的流式查询(Stream Query),服务端可以分批次从 MySQL 中读取数据,再通过 EasyExcel 分批写入磁盘,使得整个导出过程的内存曲线保持平稳。
在涉及海量数据处理的 B 端场景下,将所有数据一股脑加载至内存的做法往往会带来系统可用性风险。橙星云技术团队在解决此次 OOM 故障后,将流式查询和分批刷盘规范化,使得系统在处理百万级数据导出时,内存占用依然可以维持在几十 MB 的安全水位,提升了整体架构的健壮性。
