记录一次解决内存溢出的问题_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > 记录一次解决内存溢出的问题

记录一次解决内存溢出的问题

 2017/4/30 5:32:52  white_crucifix  程序员俱乐部  我要评论(0)
  • 摘要:一个报表工具系统,一直以来,我们这个系统经常会发生宕机的情况,服务一共由8个web实例做负载,每天几乎要崩掉4个实例的样子。从后台日志查看,全部都是由OOMerror引起,即内存溢出问题,具体来说抛了NewTLA错误,即无法创建更多的线程。这个系统已经运行很长时间,之前一直没有空好好的研究,加上对这块问题不熟悉,同时很难从日志堆栈中找到直接原因,所以只是在主机上写了一个crontab脚本,每天定时重启所有实例,让之苟延馋喘。最近狠下心好好研究下原因。整个解决过程调整了两个模块的逻辑模块1
  • 标签:解决 问题 内存
一个报表工具系统,一直以来,我们这个系统经常会发生宕机的情况,服务一共由8个web实例做负载,每天几乎要崩掉4个实例的样子。

从后台日志查看,全部都是由OOM error引起,即内存溢出问题,具体来说抛了NewTLA错误,即无法创建更多的线程

这个系统已经运行很长时间,之前一直没有空好好的研究,加上对这块问题不熟悉,同时很难从日志堆栈中找到直接原因,所以只是在主机上写了一个crontab脚本,每天定时重启所有实例,让之苟延馋喘。最近狠下心好好研究下原因。

整个解决过程调整了两个模块的逻辑

模块1:

由于不熟悉调优,只能先猜测,内存溢出无非就是逻辑中将大量对象创建在内存中,同时无法及时被GC回收。那么,介于报表工具的特性,第一个想到的就是下载模块。报表的数据总量取决于业务,多则几万,几十万。

第一阶段:
验证下载过程的内存开销情况。我本机起了工程,JVM启动参数增加了-XX:GCPrint。表里准备了7万条数据,进行了一次下载。日志显示,下载过程GC非常频繁,且每次回收后剩余占用水涨船高,导致后半段全是FullGC,大量对象无法回收的现象很明显了。

第二阶段:
接下来,就是读懂前人写的下载逻辑并修改。
下载文件包括两个方向,一是下载xls格式文件,二是下载csv格式文件。其中一个关键的逻辑缺陷在xls下载中,它通过一句sql将所有的查询结果都收集在一个List中,然后再将List遍历后写入文件。这导致了下载过程未结束时,整个List内数据都不会被GC回收。如果同时进行的下载任务较多,极有可能会撑爆内存。
然后就是重构下载逻辑,一个好的方式是边查边写,在sql查询的过程中,在resultset遍历取数的时候,就立即将数据写入文件。这样做,不需要一个List始终引用着数据,下次GC时,无用的数据就能顺利被回收。
花了一星期重构了代码(因为原代码写的相当冗长和凌乱,涉及到大量报表业务相关的处理),再一次进行了GC测试。但是这一次的结果令我相当差异!本以为GC日志情况会好很多,然而看起来仅仅改善了一点点。依然是后半段大量FullGC,如果将-Xmx调低到一定程度,也会爆炸。

第三阶段:
要找到哪些对象大量持续占用内存,首先我猜测依然是查询数据的原因,本想使用工具来进行查看,比如IntelliJ Idea的memory view,但首先我要知道大概是什么对象占用内存,然而我已经去掉了前文的List,现在根本不知道还有什么地方被引用,直接使用工具里的对象数排序,也只能看到一些基本类型占用多,char[] object[]之类的,无从下手。所以我使用了最土的办法:将各个关键环节的代码注释掉,然后看GC情况。
最终发现,是在调用了Excel工具jxl的api时,狂吃内存,具体来说就是将单元格对象Cell(含数据),不断add到Sheet对象后,一直残留在内存中。

第四阶段:
此时,停下脚本,稍微查阅一下资料,了解一点写入excel的问题。网上确实很多人都提到写入excel占用大量内存,再结合我本地的调试,发现想写入的数据,要到最终workbook.close()时才会一次性写入文件,并没有我想象中的不断写入。简单想了一下,xls不是文本文件,不可以将新数据直接append到文件尾,一定是内存里处理好了完整的数据结构,一把刷入。这应该和用jxl还是poi库来处理xls无关。这里,涉及到前文我一直没有说的一点,就是CSV下载,这部分内存开销非常的少,而且原代码也是使用了边查边写思路,从GC日志看,几乎不会超过100M的开销,而xls下载动不动就超过500M,就是因为CSV是文本文件。

这个时候就尴尬了,网上尽管也提到jxl有一个参数设置临时文件可以解决OOM问题,但是我试了下,还是不行,这个参数说的是jxl会在write的过程中先写入临时文件,然后怎么怎么。然而我研究发现,write方法也是在add所有cell之后才能调用的,仅仅比close方法早一行代码而已。测试后,这个参数确实会产生临时文件,然而内存没有改善。。。我个人认为xls只要做不到尾部直接追加新数据,那一定是存在内存中一把刷入的。

第五阶段:
所以,多人同时下载大数据量excel时,迟早会挂。既然无法解决jxl对象占用内存的问题,就要换个思路了。我想到的是将excel下载任务放线程池跑。充分考虑了该报表系统的业务使用情况,我决定一个web实例就只允许2个下载任务并发,即线程池MaxSize用2。况且我由8个web实例,同时支持16个下载任务。由于绝大部分下载任务都是比较轻量级,数据少又快速,所以不会有什么积压。

至此,下载模块的重构算是结束了,于是我兴高采烈地上了一波线

。。。
。。。

观察了一天。草,没区别!还是挂了3个实例

那么看来,下载模块不是宕机的主因。回过头来,再看了看异常信息,new tla。话说最早看到这个信息大概在一年前,看不懂加上没时间,所以解决不了。而一年以来各方面的学习都在进步,我知道这是和线程缓冲区有关,无法在其中分配新的线程空间,也就是说,持续占有在内存中的线程太多。

模块2:
第一个猜测到的地方,就是访问日志记录。原来的日志逻辑是,每一张页面表格的查询,就会记录一次日志到mq,即如果页面上如果有三张表格,就会记录3条日志;或者点击一次查询按钮,也会产生一次记录。看到这里的逻辑,我觉得有点无厘头了,这样记录日志价值不大,而且过于频繁。尽管我没有像模块1一样研究的很细,但我猜测就是因为这里导致线程瓶颈了。

所以,我决定修改日志记录的目标,只要记录每个报表页面的访问次数,即访问一次或者刷新一次才记录一条日志,相当于常说的PV。

当我具体改代码的时候,我发现一个坑,原来按页面访问记录日志的代码也是有的,只是这个分支一直都没进来。。。。于是修复了判断逻辑,再把按查询次数记录给去掉。

上线后观察,果然得到了改善,一天下来,终于不再宕机。所以确认应该这个就是主因。要知道,之前每天都会有实例宕机。


总结一下,尽管模块1不是主因,但也是存在的问题,而且过程中学习到了更多的知识,还是很有价值的;其实模块2的修改本质上是比较投机的,人为的合理降低了业务压力,并没有从技术角度去解决,还是有些遗憾。模块2的技术角度解决,我想了下思路,是日志任务线程池的问题?还是日志发送给mq的瓶颈?亦或者是发送前处理日志内容的问题(妈的逻辑里还有一个select查询,我也是醉了)。然后是调整线程池参数?还是mq参数,亦或者是优化发送前的逻辑代码?

然而我累了。。不高兴研究了 :p
发表评论
用户名: 匿名