欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

一次JVM OOM问题的解决 javaoom内存溢出 

程序员文章站 2022-03-17 16:24:04
...
前不久其实写了一篇,但是由于当时没有踩到重点,所以经过这段时间的研究,终于把这个内存溢出问题彻查清楚了

背景:
我们的一个报表工具系统,核心功能当然是查看和下载,其中下载文件功能需要将报表数据都写入文件中。一直以来,系统总是会因为JVM内存溢出而宕机。

现象:
从 weblogic 日志里看,宕机前抛出了大量java.lang.OutOfMemoryError: getNewTla错误信息,而且堆栈信息中能出现各种情况,而且有的很抽象,难以看出具体由某一个功能某一个方法导致的。后来想想,内存撑满后,确实可能导致其他功能崩坏。

由于之前对JVM及检测调试手段都不熟悉,因此通过一定时间的看书学习,期间也一直做些尝试

第一次尝试:
阅读了下载功能的代码后发现,逻辑中先进行了一次全量 sql 查询返回一个 List,然后把List遍历写入文件。这个明显不对,应该利用ResultSet批次查询的特性边查询就边写入文件。不然下载过程List 持有的大量数据对象会占用很多内存。

但是改造后,效果只有轻微的提升,通过在JVM启动参数添加 -XX:+PrintGC,查看下载过程的垃圾回收情况。内存开销依然水涨船高,每次 gc 的效率都很低,很快就吃了几百M。

第二次尝试:
关注点放在了写入的文件上,文件是 xls 格式的 excel 文件,经过一定了解,我得出这么一个结论:excel 文件不是文本文件,无法直接将数据写入文件尾部,无论是 jxl 还是 poi 这种 excel工具库,一定是将全量数据以某种数据结构存于内存中,最后一次性写入。因此这部分理论上是无法优化的。

其实到这里,有点懵了

第三次尝试:
这时候通过阅读代码已经很难猜测到还有什么地方可以优化了。于是想着将下载过程中的内存快照给 dump 出来,然后通过工具看能否分析出什么。

我的做法是:先通过 IDE 在逻辑结束前设置断点,然后在命令行通过 jmap 命令,生成当前内存快照的 dump(hprof) 文件。最后通过分析工具 MAT 打开 dump 文件。

工具显示,占用内存最大的两部分,一部分和excel 工具 jxl 的某个类有关,占了70,还有一个和 sql 查询某个类有关,占了30%。

a. SqlRowSet
这个对象持有了30%的开销,于是在代码中搜到了这行 SqlRowSet rs = this.getJdbcTemplate().queryForRowSet(sql);
了解后知道,这个类是对 ResultSet 的一种扩展,用法和 ResultSet 极为相似,区别在于,SqlRowSet会持有全量的查询数据,那么问题就在这里了,这里居然也有一份全量数据的引用。这就很尴尬了,由于代码里变量名都是用的 rs,而且用法一样,导致之前一直以为用的是 ResultSet.....  修复完,再进行dump分析,这部分的开销确实消灭了

b. WritableCellFormat
这个类持有了大部分的内存开销,依然再代码中找到了使用的地方。原来这个类是作为 excel 单元格对象附加的一个 Format对象。而代码中对每一个单元格,都去 new 了一个 format 对象。通过 MAT 工具能查看每个对象实例的大小,一看200B,数量一多内存直接上去。这里应该改成:只需要每一列 new 一个 format 对象,同一列的单元格共用一个呗

这两个问题改完以后,确实问题都解决了。回过头来看,如果一开始就比较熟练的话,完全可以将启动参数 Xmx调小,同时加上 -XX:+HeapDumpOnOutOfMemoryError参数使得再 OOM 后自动生成 dump 文件,再用分析工具查看对象占用情况,快速解决问题。不过现在也是一个学习的过程,挺好。