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

Tomcat内存溢出问题排查分析

程序员文章站 2022-07-15 14:46:06
...

目录

问题背景

分析原因

解决方案

思考


问题背景

前段时间,公司一个老系统从resin4换成了Tomcat8.5,jdk也由1.6升级到了1.8(项目过于老,没敢升级到最新jdk),用nginx做了反向代理,部署完成,启动服务后,一切看起来都很顺利。但是不到一天的功夫就有人反映系统很卡,然后就是跳出来nginx的错误页面。赶紧连上服务器,重启Tomcat服务,问题暂时解决。

分析原因

查看Tomcat日志,发现有这么一行:

java.lang.OutOfMemoryError: GC overhead limit exceeded

Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。增加-XX:-UseGCOverheadLimit 这个参数可以禁用检查,但其实根本解决不了问题,内存溢出了,首先我想到的是增加内存,之前照着原来resin4配置的内存,-Xmx只有512m,我增加到了1024m,但没过多久,系统再次崩溃,于是又增加内存:

-Xms3072m
-Xmx3072m
-Xmn1024m
-XX:PermSize=128M
-XX:MaxPermSize=640m

还是不行,显然,问题不在内存大小上。

于是再增加配置,配置jconsole监控jvm,并在内存溢出时候自动生成堆转储文件:

-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=localhost
-Dcom.sun.management.jmxremote.port=9090
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.local.only=true
-XX:+HeapDumpOnOutOfMemoryError

jconsole界面:

Tomcat内存溢出问题排查分析

同时也可以打开http://localhost:8080/manager/status查看内存情况

Tomcat内存溢出问题排查分析

发现老年代(PS Old Gen)内存增长虽然不是很快,但在一次full gc之后仍然占到了60%以上,每次增长一点,这样一来,几次full gc之后,老年代内存被耗尽。

那到底是什么情况导致老年代内存回收不掉呢?只能分析堆转储文件了。

堆转储文件有几种生成方式:

  1. 配置参数-XX:+HeapDumpOnOutOfMemoryError,可以在内存溢出时自动生成
  2. 试用jdk自带命令jmap -dump:format=b,file=heapdump.hprof
  3. 试用jconsole生成

我先用jconsole执行一次gc,回收掉一部分内存,剩下的就是回收不掉的,然后生成堆转储文件。

接下来用Eclipse Memory Analyzer分析堆转储文件。

Tomcat内存溢出问题排查分析

饼图上最大的853M这部分就是问题所在了,那这部分内存里到底放的什么呢?

打开dominator tree

Tomcat内存溢出问题排查分析

很明显,JspRuntimeContext类下一个ConcurrentHashMap对象占了大量的堆内存,进一步展开,map里面放的全是系统客户联系记录的静态页面linklog.html,系统将联系记录保存数据库的同时,生成静态页面,然后在jsp页面用jsp指令动态加载:

<jsp:include page="<%=linkLogUrl%>" flush="true"/>

这是一个动态加载指令,先将包含的页面编译,然后加载到引用页面。反编译分析JspRuntimeContext源码

Tomcat内存溢出问题排查分析

一个成员变量jsps是ConcurrentHashMap类型,当浏览器请求页面时,Tomcat会查询map里面是否有这个页面对象,如果没有则编译页面并放入map中。系统中客户数据有很多,每个客户生成一个,于是生成了大量的联系记录静态页面。这样每次请求不同的客户信息页面,就在map里存放越来越多的页面对象。ConcurrentHashMap是一个高效且线程安全的HashMap实现,缺点是占用内存高,以空间换取时间。

解决方案

找到了问题原因,那就有解决思路了:

String logContent = FileUtils.readFileToString(fileLog,"UTF-8");

读取文件内容,然后在页面输出。这样就不会编译大量的静态页面并放到map里了。更新代码后,观察了一段时间,果然,full gc之后老年代内存也降到了5%,回收率很高。至此,系统稳定了,再也不会因为内存耗尽而挂掉。

思考

那为什么之前resin4仅仅512m的内存都没有出现这种问题呢?我想,java提供了servlet标准,resin和Tomcat作为容器,有着各自不同的实现,resin对servlet规范的具体实现我没有去详细研究,我相信resin并没有像Tomcat那样把大量的静态页面也编译放到一个ConcurrentHashMap中,所以同样的代码,resin没有内存溢出的问题。

解决问题,思路很重要,分析一个问题,通过观察现象,找出可能导致出现这个现象的原因,顺藤摸瓜,抽丝剥茧,最终解决问题。

相关标签: java 内存溢出