生产环境隔一段时间就会发生内存溢出,每次都是重启服务临时解决问题,这次使用mat分析了一下到底什么问题导致的内存溢出,从根本上解决一下这个问题,使用的工具就是mat(Memory Analyzer Tool)
发现问题:
服务器宕机,错误日志报错内存溢出,并且好多gc overhead limit exceeded
出现gc overhead limit exceeded,说明出现多次gc对象没有被回收掉的情况,猜想是有大对象没有被回收掉,下面就来验证一下
分析过程:
1.从生产上拿到dump文件,具体生成dump文件的过程这里不做解说
2.使用mat 打卡hprof文件
从整体情况可以看出,2.3 gb的堆内存,有大对象占用。
怀疑是有内存泄漏,我们通过Leak Suspect Report报告查看
3.点击下面按钮,选择Leak Suspect 查看报告
报告显示有4个问题:
显示org.springframework.boot.loader.LaunchedURLClassLoader 加载com.mysql.cj.jdbc.result.ResultSetImpl的时候占用了561M的空间(后面验证是查询数据库加载到内存的数据太多导致)
显示org.springframework.boot.loader.LaunchedURLClassLoader 加载com.mysql.cj.jdbc.result.ResultSetImpl的时候占用了561M的空间(后面验证是查询数据库加载到内存的数据太多导致)
显示http-nio-9010-exec-12这个线程持有本地变量多达555M
显示http-nio-9010-exec-19这个线程持有本地变量多达403M
以Leak Suspect 报告第一个问题为例,我们点开内存泄漏报告的Detail,查看其详情
Shortest Paths To the Accumulation Point视图显示如下
可以看出正是和org.springframework.boot.loader.LaunchedURLClassLoader
这个GC root相连导致当前Retained Heap占用相当大的Object对象无法被回收,而对象数量居然达到了360145个。
Accumulated Objects in Dominator Tree视图,可以看出Object对象中,到底是什么内嵌对象占用heap高。
可以看出,360145个Object对象数组内部,主要是ByteArrayRow对象
我们继续向下看,通过按class类分组,来看看具体占用比例情况。
按class分组后,ByteArrayRow对象占用了560M
右键点击 com.mysql.cj.jdbc.result.ResultSetImpl @ 0x6a0fe1a40,在弹出菜单中点击 Path To GC Roots → with all references:
可以看到该对象到 GC Root 的各条路径:
一个对象可能有多条路径通往多个 GC Root。但一般来说,其中某一个 GC Root 必定是一个线程。从上图可以看到 com.mysql.cj.jdbc.result.ResultSetImpl 对象通往 GC Root 的路径有很多条,但最终都殊途同归到了线程对象 org.apache.tomcat.util.threads.TaskThread @ http-nio-9010-exec-12。这就是说,是 http-nio-9010-exec-12 线程在运行过程中创建出了 com.mysql.cj.jdbc.result.ResultSetImpl 对象。那么我们接着看看 http-nio-9010-exec-12 线程到底在干什么?
双击展开该线程的堆栈,接下来就要重点观察那些我们自己写的类里的方法,这里需要凭借一些对自己代码的熟悉度和经验了。我们可以看到堆栈中有调用到 SysApiServiceImpl.queryOrder方法, 打开可以看到里面的参数信息,policyNo和orderCode 的值为空,orderStatus值为00,01,03,10,20,30,40,50,60,61,62,63,D0,D1,D2,D3,D4,11,12,13,14,15,16,32,I0,I1,I2,I3,I4,11,12,13
第二个问题和第一个问题是一类,第三个和第四个问题是一类问题
第三个问题定位基本和第一个问题步骤一样,只不过在内存泄漏报告的Detail页面,可以直接看到堆栈信息,定位到方法
总结,第三个和第四个问题不同的线程调用的方法一样,加载大量数据导内存中,导致了第二个问题和第一个问题