一次JVM OOM问题的解决 javaoom内存溢出
程序员文章站
2022-03-17 16:23:58
...
前不久其实写了一篇,但是由于当时没有踩到重点,所以经过这段时间的研究,终于把这个内存溢出问题彻查清楚了
背景:
我们的一个报表工具系统,核心功能当然是查看和下载,其中下载文件功能需要将报表数据都写入文件中。一直以来,系统总是会因为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 文件,再用分析工具查看对象占用情况,快速解决问题。不过现在也是一个学习的过程,挺好。
背景:
我们的一个报表工具系统,核心功能当然是查看和下载,其中下载文件功能需要将报表数据都写入文件中。一直以来,系统总是会因为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 文件,再用分析工具查看对象占用情况,快速解决问题。不过现在也是一个学习的过程,挺好。