一次JVM性能调优实践

485 阅读4分钟

背景

我们有个发送短信告警的服务,在线上环境跑着跑着就挂了,发不出短信,但重启后又能正常接着跑,只是跑了差不多两三个小时后就又会挂,当时运维不懂只能定个闹钟提醒自己去重启该服务,后来由我排查这个问题。

当时环境:
JDK版本:jdk1.6.0_10
系统:windows

排查

发送短信服务挂了是会有日志,找运维帮忙查看日志 image.png 看了日志,说是报了OOM,当时也不是很清楚这块,第一次遇到这种问题,百度谷歌查了很久,发现内存不够用的时候服务就会挂,于是就想是不是分配给Java服务的内存不够,而Java服务启动是可以指定分配给JVM的堆的大小的,当时那台机器的配置很高,内存有32G,于是就直接分配给了JVM 5G的堆内存,而初始的堆内存是1G。使用这个命令:

java -Xms1024m -Xmx5120m -jar demo.jar

这样启动是有效果的,过了两三个小时还能继续发短信,以为就没问题了,于是高枕无忧的去睡觉,隔天回来运维大哥说凌晨短信又发不出了,接着看日志,发现又是OOM。敢情这样操作还是会挂,只是把挂的时间延长了而已。。。

意识到得搞明白原理才能彻底解决这问题。先是了解了OOM的知识。

内存溢出与内存泄漏

  • 内存溢出(Out Of Memory):系统已经不能再分配出你所需要的空间,存储的数据超出了指定空间的大小
  • 内存泄漏(Memory Leak):程序在申请内存后,无法释放已申请的内存空间,导致不断创建新对象而又不被垃圾回收,被占有空间越积越多

内存溢出的原因有很多种,从上面日志图片可以知道发生内存溢出的区域是Java heap space,说明是堆的溢出,而其中内存泄漏是会导致内存溢出的。

一开始的做法以为没有内存泄漏的问题,觉得只要加大堆内存就可以,但结果也知道,加大堆内存只是延长了OOM出现的时间,这说明是存在内存泄露的。那具体是哪块代码呢?
先要知道有哪些Java线程,然后打印出对应的Java线程的堆栈信息,分析堆内存中占有空间最大的是那几个类。

  • 查看当前已启动的JAVA进程和对应的PID
jps -l
  • 查看堆栈信息,对应的线程的状态
jstack pid
  • 生成java堆中对象的相关信息,包含对象实例数量以及占用的空间大小并输出到jmap.log文件
jmap -histo:live PID > /tmp/jmap.log
  • 打印出某个java进程的jvm dump文件
jmap -dump:format=b,file=heap.bin <pid>
  • 也可以读取名为 heap.bin的文件,并监听7000端口,可在浏览器localhost:7000打开
jhat -J-Xmx512m heap.bin

(因为有时dump文件很大,所以加了-J-Xmx512m参数)

image.png

获取到dump文件是需要通过工具来分析的,我是用MAT,也可以用visualVM。我这里用的是MAT。

image.png

经过MAT分析, 这下子一目了然:
sun.net.httpserver.ServerImpl$ServerTimerTask实例占了堆内存 88.33%,很明显这个是有内存泄漏的。我在JDK源码中找到这个类,同时debug了一遍,也找到了对应JDK BUG清单,证实了这个bug的存在: bugs.java.com/bugdatabase…

image.png 说下JDK1.6源码造成内存泄漏的原因和解决方法:

  • 每当客户端与 HttpServer 连接时,HttpConnection 实例会被加入到 Set<HttpConnection> allConnections 中。那么当一个连接关闭时,理应将其从 allConnections 中移除。
  • ServerTimerTask 只处理超时连接,检查和移除那些在 idleConnections 中的连接。如果一个连接是bad request,它不会被添加到 idleConnections,因此不会被 ServerTimerTask 移除。、
  • 由于bad request的 HttpConnection 从未被移除,它们将持续存在于 allConnections 集合中,导致内存泄漏。这样,随着时间的推移,连接的数量会不断增加,即使它们已经关闭。从而造成了内存泄漏、
  • 链接给出了解决方法,就是遍历allConnections的所有连接,并检查是否已经关闭,将已关闭的从allConnections里移除,将未关闭的连接收集到一个list,然后clear掉。
  • 这个bug在后续版本 com.sun.net.httpserver.HttpServer 被修复了

另提

  • 在生产中一般将-Xms和-Xmx设为为一致,是为了避免频繁扩容和GC释放堆内存造成的系统开销/压力。如果-Xms起初值设置的比较小,因为内存小,所以JVM需要不断回收以释放内存就会频繁触发GC操作,当GC操作无法释放更多内存时,才会进行内存的扩充。

参考链接