我网站不知道什么时候,开始内存飙升,从 Tomcat 启动后,初始内存占用4%~5%
左右,到20%、40%
最后服务器卡死,SSH都连不上服务器,不得不重启。但是我知道是我程序的问题。然后分析问题,解决问题。陆陆续续持续了一个多月,下面分享解决思路。
一、定位造成内存溢出可能存在的问题
- io流操作文档没关闭流。
- 往一个静态集合变量里一直压栈。
- 连接没释放。
- Java队列没消耗。
- Ehcache缓存使用量过大。
- 频繁IO操作大文件。
- Session过期时间太久。
- 等等.....
我定位有可能造成的原因是以上原因,针对本站的特点在做细排查,有可能出现的问题。
- io流操作文档没关闭流。(有可能)
- 往一个静态集合变量里一直压栈。(没有这个问题)
- 链接没释放。(有可能,因为本站有大量的HttpClient请求)
- Java队列没消耗。(有可能,因为本站使用上了)
- Ehcache缓存使用量过大。(没使用)
- 频繁IO操作大文件。(没有)
- Session过期时间太久。(可能有)
- 等等.....
二、采用Memory Analyzer Tool(MAT)分析Java内存
采用 jmap 命令(Java Memory Map)导出内存转储快照(Dump);
首先查询到你对应的 Tomcat 的pid
ps -aux|grep xxx-tomcat
然后执行jmap命令:
jmap -dump:format=b,file=73630.hprof 16706
导出完毕。down下来用 Eclipse ,或者 MyEclipse 查看,但是 MyEclipse
或者 Eclipse
要先安装工具,自行百度。然后以openFile
的方式打开。如图:
可能有点看不懂,自行解决,点击Histogram
,可以看到内存中的详细信息。
可以看到char[]
、byte[]
占用的是最多,而且不是多一点点。这明显不正常。就是一些IO流相关的信息。Memory Analyzer 工具还是有很多功能的,我也不太会用。具体可以多看看相关的博客。下面来排查问题。
三、问题逐一排查,由容易到复杂
3.1 Session检查
从配置文件web.xml
查看,发现 Session 超时配置了900
分钟。。。醉了,回想起来,是当时因为有权限校验(防止攻击)模块利用 Session 来实现,所以才出此下策。改成30
分钟,重启后效果有一点点。继续排查。
3.2 IO流操作没关闭检查(严重)
全局搜索各种InputStream
、OutputStream
,各种Buffer
等等,然后各种修改关闭。尤其是本站的 HTTP 模拟请求工具,一天的用量非常大。如下IO
流在finally
里try...catch
各种关闭。
try{
//......
}catch(Exception e){
//......
}finally{
realUrl = null;
try {
if(null != conn)conn.disconnect();
conn = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "请求完毕关闭流出现异常,可以忽略![%s]", url);
}
try {
if(null != outStream)outStream.close();
outStream = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "请求完毕关闭流出现异常,可以忽略![%s]", url);
}
try {
if(null != out)out.close();
out = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "请求完毕关闭流出现异常,可以忽略![%s]", url);
}
try {
if(null != inStream)inStream.close();
inStream = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "请求完毕关闭流出现异常,可以忽略![%s]", url);
}
try {
if(null != in)in.close();
in = null;
} catch (Exception e2) {
LoggerUtils.fmtError(HttpManager.class, e2, "请求完毕关闭流出现异常,可以忽略![%s]", url);
}
double end = System.currentTimeMillis();
map.put("time", (end - begin) / 1000);
//大对象用完赋值null
bo = null;//促进回收
}
结论:全部加好后重启,过一段时间再看。效果不明显。其实是我在平时代码严谨上这个错误没有出现,但是从经验角度来说,如果这个没处理好,这个是最容易出现 内存溢出 的。
ps:关于后面有一段代码,bo=null;//促进回收
,我个人是这么理解,不知道有没毛病。主要是针对局部变量的大变量。可以用完后赋值为null
。
3.3 HttpClient请求链接释放问题(严重)
Httpclent 请求链接不主动关闭,这个问题也是个大问题,但是对内存的影响,看从什么角度,占用最大的应该还是响应链接,把链接用完了,新的链接就进不来,我们知道 Tomcat 默认配置一共好像才150
个,一会就用完了,如果不用完关闭,那么会造成链接释放慢,甚至不释放。如果不释放,请求得到的responseBody
那么有可能一直没有释放了。
HttpClient怎么释放?
其实百度一下有很多答案,我这里顺便带一下。
1.请求头增加关闭Head信息。
//添加头信息
HttpURLConnection conn = null;
URL realUrl = new URL(url);
// 打开和URL之间的连接
conn = (HttpURLConnection) realUrl.openConnection();
//省略部分代码
//增加请求完毕后关闭链接的头信息
conn.setRequestProperty("Connection", "close");
2.用完Httpclent后手动关闭
//添加头信息
HttpURLConnection conn = null;
URL realUrl = new URL(url);
// 打开和URL之间的连接
conn = (HttpURLConnection) realUrl.openConnection();
//省略部分代码
//手动释放
if(null != conn)conn.disconnect();
3.通过线程的方式扫描关闭。
这个方法其实类似启动一个守护线程(一直启动着),来扫描有没有关闭的请求。这个方法比较鸡肋,用的好就很好,用的不好就蛋痛了。推荐使用方法一、方法二,为了保险起见你可以两种一起使用,并不会有问题。
总结: Httpclent 链接请求完毕一定要关闭。有的人可能会看了浏览器里的请求头信息为:Connection:keep-alive
,这个回头我会详细说明,但是这个是浏览器需要的,因为你还要继续加载css、js、image
等等,大概是这个意思,而你的 Httpclent 只需要加载一次,所以直接close
即可。
3.3 Java队列(最终问题定位)
昨晚把 队列 换成了阿里的队列,问题解决了,几个小时过去了,还是5.6%
。
换成了阿里的 队列 ,我把队列用于本地计算机跑,线上跑网站,把所有队列的错误信息,以及执行情况看了一下,发现是之前逻辑写的有问题,导致队列异常,队列异常没有catch及时处理。故导致了这个现象的最大的罪魁祸首。