jvm调优之内存泄漏分析

  一、前言

      我们都知道严重的内存泄漏会导致内存溢出,内存溢出最终会导致程序崩溃。前段日子,我几乎被这个问题搞到内分泌失调,每个晚上都过得提心吊胆的,生怕一个微信或者电话过来说项目挂了。哎,说多了都是泪,直接进入主题吧。

二、问题描述

1.部署环境

      linux;

2.问题发现

        项目刚上线的时候,就出现响应慢的问题,甚至直接就502。首先想到的是数据库的问题,然后在机器上安装了mysql客户端,连接线上的数据库(连接命令:mysql -h(ip) -u(用户名) -p(密码)),随便select了一张表,查询效率都没问题。这下没辙了,去看一下日志吧。发现都是broken pipe的异常(客户端发送请求服务器长时间没响应),再仔细一看,这个异常差不多都是在产生java.lang.OutOfMemoryError之后才出现的。赶紧打开top命令一看,情况也是在预料之中,cpu飙到了百分之九十多。

     top:

       f 或者F 从当前显示中添加或者删除项目。

  o 或者O 改变显示项目的顺序。

  l 切换显示平均负载和启动时间信息。

  m 切换显示内存信息。

  t 切换显示进程和CPU状态信息。

  c 切换显示命令名称和完整命令行。

  M 根据驻留内存大小进行排序。

  P 根据CPU使用百分比大小进行排序。

  T 根据时间/累计时间进行排序。

  3.内存泄漏分析过程

      根据top命令查出哪些线程在占用cpu,导致cpu的使用率一直高居不下。

   (1)通过jps、ps或者其它方法,找到java的进程号PID;

   (2)top -p <pid>按H(当前进程的所有线程)找出cpu占100%的线程号,因为找出的线程号是二进制的,需要转换为十六进制;

发现有八个高cpu线程在做gc垃圾回收:

   (3)使用jstack -l <pid> |grep <转换来的十六进制数>,查看线程正在做什么。线程在dump中有三种状态:RUNNABLE(执行中)、BLOCKED(被阻塞)、WAITING(等待中)。如果发现结果类似这种:"Concurrent Mark-Sweep GC Thread" prio=10 tid=0x0000000053293800 nid=0x*** runnable,说明jvm正在标记清除该线程,从而可以知道jvm正在忙着垃圾回收。由于jvm根据对象的存活周期不同将内存划分为几个区。分为年轻代和年老代,年轻代的对象大多都是小对象,jvm会频繁进行回收,存活下来的对象会根据复制算法从from区到to区,只需要付出少量存活对象的复制成本就可以完成收集。而年老代的对象存活率都比较高,并没有额外的空间帮它分配压力,就使用“标记-整理”算法进行回收。

(4)使用jmap -heap <pid>查看堆内存情况,发现年老区的内存被撑到了99%,至此也不难理解为什么程序响应慢或者直接挂了,cpu都忙着垃圾回收,哪有时间去管其它事情;

(5)内存溢出也有可能是机器分配的内存不够导致,于是我分配了8g的内存(xmx8g)给它,发现程序启动没多久,cpu又跑到百分之九十多了。发现根本就不是这个问题,才意识到垃圾没有回收,加再大的内存也会被消耗掉,于是开始着手代码优化了。

(6)代码优化?从何下手呢。必须要找到哪些对象没有被回收,哪些对象占用的空间最多。于是,我从服务器dump了个堆文件下来分析(命令:jmap -dump:live,format=b,file=head.xx <pid>),这个文件可能有点大,拿下来的话,最好压缩一下(命令:tar -czf dump.tar.gz dump.xx)。不拿下来的话,也可以用jhat命令进行分析详细的就不说了。(注意:用这个的话,要看一下机器的内存是否够,否则机器有可能会挂)

(7)文件拿下来后,需要用专门的内存分析工具。其中,有两个工具可以用,一个是jdk1.6以上自带的工具:jvisualvm,不过这个工具分析5G的堆文件相当吃力,等待的时间比较长,而且结果也不够直观。所以推荐使用第二种:eclipse的插件Memory Analyzer(下载:http://www.eclipse.org/downloads/download.php?file=/mat/1.8.1/rcp/MemoryAnalyzer-1.8.1.20180910-win32.win32.x86_64.zip)。如果打开的堆文件很大,需要修改一下MAT的启动参数(memoryanalyzer.ini 中的xmx参数);

(8)打开后发现用户表的对象占了很大一部分没有被回收,于是想到用户表的很多字段没有用的但是没有去掉,是不是在别的地方用到了,导致了对象没有被回收,于是把所有没用的字段都去掉,更新。结果没过一会又挂了,内心已到崩溃的边缘。。。

(9)使用jmap -histo <pid>也可以查看推内存占用情况,使用这个命令查看,发现还是用户表的对象占用了很多的内存,问题还没解决......

(10)于是没办法了,自己写个测试呗。你不是不回收吗?我看你怎么就不回收了,在用户类里面重写了一下垃圾回收的finalize()方法,调用了一下JPA的保存方法,发现这个类果然没有被回收。仔细研究发现这个类用到了hibernate的缓存技术,会不会是缓存引起的呢(缓存一种用来快速查找已经执行过的操作结果的数据结构,因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡)。于是把缓存去掉,重新测了一遍,发现对象终于被回收了。

3.总结

      后来细细想了一下,十多万的用户,在线的人多了,由于缓存的原因,用户对象一直没有被回收,而且还一直堆积,才导致了内存的溢出。

     原以为问题就此解决,结果第二天被告知,项目又挂了,心一下子又悬起来了。赶紧打开机器看了一下top信息,发现cpu正常 ,再看一下带宽,原来是带宽不足了......

     

猜你喜欢

转载自blog.csdn.net/weixin_39886135/article/details/83791601