Java服务自动停止,Java服务内存溢出问题解决记录。
过程描述
服务器上的一个项目突然服务不了了,登录服务器一看,服务被停了,第一反应大概率就是内存溢出导致的,结果查看日志没有任何报错,就很奇怪,然后就在启动命令里面加上了一个命令,该命令的作用就是在发生内存溢出的时候会自动生成dump文件(jvm内存快照),把该文件下载到本地后用jvisualvm应用打开就能分析出问题。
# 当OutOfMemoryError发生时生成dump文件,-XX:HeapDumpPath指定生成后的文件存储路径
java -jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/test/dump/ -Xms6g -Xmx6g demo-0.0.1-SNAPSHOT.jar
过了一段时间服务又停了,日志依然没有报错,也没生成jvm内存快照文件,从这个情况来看应该是没有触发jvm内存溢出,否则已经生成内存快照文件,然后使用命令查看系统日志:
## 切换到系统日志目录
cd /var/log
## 查看系统日志,grep 过滤一下,只查看Java相关的日志
sudo cat messages* |grep java
日志显示如下:
[zhh@official-website ~]$ cd /var/log
[zhh@official-website log]$ sudo cat messages* |grep java
Sep 3 01:49:38 official-website kernel: [27946] 1000 27946 2661073 1546426 3236 0 0 java
Sep 3 01:49:38 official-website kernel: Out of memory: Kill process 27946 (java) score 773 or sacrifice child
Sep 3 01:49:38 official-website kernel: Killed process 27946 (java) total-vm:10644292kB, anon-rss:6185704kB, file-rss:0kB, shmem-rss:0kB
Sep 21 01:49:48 official-website kernel: [ 2821] 1000 2821 2664978 1518600 3208 0 0 java
Sep 21 01:49:48 official-website kernel: Out of memory: Kill process 2821 (java) score 760 or sacrifice child
Sep 21 01:49:48 official-website kernel: Killed process 2821 (java) total-vm:10659912kB, anon-rss:6074400kB, file-rss:0kB, shmem-rss:0kB
从日志可以看出,由于Java服务占用的内存太多了,为了能保证系统的正常运行,操作系统就把Java服务kill停止了。为什么没有触发Java内存溢出的报错呢?因为设置的堆内存太大,实际使用的堆内存还没到达临界点。
这里说明一下什么情况下会触发jvm内存溢出的报错,一般两种情况:
1:实际使用内存达到设置的堆内存参数值(-Xmx)附近。
2:实际使用内存没有到达设置的堆内存参数值(-Xmx)附近,但是jvm向操作系统申请不到内存了,也就是说系统内存可能被其他服务占用或者设置的-Xmx参数过大。
继续排查,服务是部署到阿里云的服务器,所以是有内存监控的,于是就去看了内存监控,发现内存大小的占用随时间成梯度增长,涨上去就不会下来,说明内存没有被垃圾回收,那大概率就是代码写的有问题。
要找到是哪块代码有问题,最好的方法就是拿到jvm内存快照文件。通过查看内存监控后对比日志得到一个重要的信息,日志长得最猛的时间段就是系统一个视频会议功能使用的时间段,果然,在某次会议结束后进入阿里云内存监控界面一看,内存不出所料的飙升了。
然后就乘没人使用系统的时间段登录服务器,执行生成jvm内存快照的命令:
# 替换<pid>为Java进程的ID,file:输出文件名为heap.hprof,可自定义路径
jmap -dump:format=b,file=heap.hprof <pid>
注意:生成jvm内存快照的时候会影响系统的正常使用,最好是在系统闲置的时间段生成内存快照。而且如果系统内存快被占用满的时候是没法生成jvm内存快照的,因为生成内存快照也需要使用一点的内存。
jvm内存快照文件分析
拿到jvm内存快照后就使用jvisualvm打开,ps: 从jdk1.8以后包括最新几个版本的jdk1.8就已经不带jvisualvm了,如果需要jvisualvm只能自己到oracle官网去下载,新版的jvisualvm功能更齐全,不过目都是英文版。
导入内存快照文件后,界面显示一大堆对象和类,看得眼花缭乱,先不着急,先找找看看我们项目里面的类或者是对象,然后顺藤摸瓜即可,因为项目里面的对象都是互相引用的,jvisualvm会一一显示出来。
通过jvisualvm就能大概看出问题的所在了,界面显示有两个对象(FileInfo、Result)就是我们项目里面定义的,在内存中存在16万个FileInfo对象和12.7万个Result对象,通过界面的Reference引用栏和GC Root 垃圾回收根节点引用栏可以看到是被一个static变量cacheField引用,变量cacheField存在于类ReflectUtils中,好了,大概知道问题代码在哪了。
打开项目代码,看看什么情况:
结合上下文分析就可以知道,项目里面定义了一个static 变量 Map对象作为缓存存放部分对象,需要的时候从里面取,如果里面没有就放进去,只会往里面放,不会清除,最终导致Map里面存放的对象越来越多,除了重启服务,Map里面的对象是不会被清除的,因为是static修饰的类静态变量。
问题找到了就好办了,去掉缓存或者使用其他缓存方案。