Java堆dump文件分析
更新日期:
本文主要使用Eclipse的MAT工具和JDK自带的jvisualvm工具来分析程序运行异常之后的堆dump文件,以找到程序出错的原因。
Eclipse的MAT(Memory Analyzer Tool) 是基于heap dumps来进行分析的,它的分析速度比jhat快,分析结果是图形界面显示,比java内置jhat的可读性更高。官网上有下载地址,也可以在Eclipse Marketplace中找到安装,安装可以参考这里。
成功安装完之后,打开Eclipse,通过Window->Open Perspective->Memory Analysis,进入MAT的工作面板,如果看不到Memory Analysis,说明没有安装成功。然后就可以通过File->Open Heap Dump...来打开待分析的堆dump文件。
打开oom.prof(一般是.hprof扩展名)文件,产生该文件的程序在运行时报OOM,之后看到如下概览(Overview)界面,
直觉的反应是,堆的总大小是5.8M,但是有一个类的对象占用了其中的5.4M,很大概率,问题就在这个类上。切换到柱状图模式,并按照Shallow Heap1倒排序,如下
可以看到有一个char[]
的类占用了过多的内存,有1026个对象,内存占用量第一,占用第二的对象所需内存不到它的1%。对这个类,右键->List objects->with outgoing reference,可以看到这个类的对象,和对象引用到的其他对象(如果选with incoming reference,则是这个类的对象,和引用到这些对象的其他对象),同样,也按照Shallow Heap倒排序,如下,
可以看到有很多char[40090]
的对象,大约70+个,每个大小都是80k(80,192 Byte),所以这些对象的总大小就是80k*70=5400k,也就是5.4M,和最初在概览界面里面看到的5.4M吻合,再向下翻页,发现还有一些其他的char[]
对象,不过都占用空间比较小,不构成压力,如下,
这些比较小的char[]
对象,看了一些内容,都是一些不太常见的字符,猜测可能是和程序字符集相关的字符编码。
下面猜测一下这个程序的逻辑和发生OOM的原因。首先,程序的堆文件大小总共是5.8M,可以猜测,该程序在运行时设置了JVM参数-Xmx 6M
,也就是最大堆大小是6M,类似这样的选项。然后,程序处理的逻辑应该大致是这样,在一个循环中,不停的新增char[]
,并且不做垃圾回收,所产生的对象内容是从0开始的整数序列toString的结果,因为可以明显看出,这些对象的内容是重复的。在循环重复执行了70+次的时候,再次新增char[]
对象的时候,堆内存不足,发生了OOM,而图中的char[36862]
对象,很可能就是最后发生内存不足的那个对象,因为这个对象的前部分内容是和其他对象的内容一致的,而长度却不足。如果的确是这样的话,这个char[]
对象的长度实际上应该是不断增长的,那么这个对象很有可能是String
类型,因为String是包含一个char[]
对象的。
实际上,该文件的domain tree页面,也印证了上述想法,如下
可以很明显的看到在ArrayList对象下面有个Object[88]的对象,这是应为ArrayList是用数组方式实现的,而在Object[88]对象下,有很多个String对象,这些对象每个占用1.32%的内存,总共占用了93.45%的内存,有了这些信息,不难分析出发生OOM的代码,应该是类似下面这样,
到此,分析完毕。
下面,再用jvisualvm来分析一个Tomcat发生OOM的堆dump文件。
先运行jvisualvm,加载发生OOM的堆dump文件tomcat.hprof。可以看到概要视图,如下,
Tomcat负责管理连接的类是org.apache.catalina.session.StandardSession
,因此只需要关心这个类有多少个实例。在类视图中,可以发现连接有9963个,如下,
对其右键->在实例视图中显示,可以看到具体的类结构
在OQL控制台视图中,可以使用OQL语言2,观察这些Session分别是什么什么时候建立的,如下,
查询的creationTime
列就是Session建立的时间,使用一个长整型来保存,单位是毫秒ms。查询得到的结果大概是下面这样,
然后,我们可以分析,1秒中内建立的最多的连接是多少个,也就是Tomcat处理每秒的峰值。可以写一个Java程序来协助求解,这个程序需要打开tomcat.hprof文件,并用OQL引擎,在这个dump文件上执行OQL才能获取全部的记录。需要使用visualvm的一些库,这些库在JDK的安装目录下,主要是以下一些目录,
由于本例比较简单,实际上只用到了其中2个jar文件,分别是,
完整的Java程序如下,
上述程序的处理步骤是:先打开堆dump文件,并构造OQL引擎,接下来在OQL引擎上运行OQL获取所有的新建Session的时间点,存入listTimes
,并排序。然后,以每个新建Session的时间点为起始点,计算1秒内有多少个新建的连接。最后,找到最大的那个连接数,并返回对应的起始时间点即可。当然,这个算法并没有很优化,但是也可以得到问题的解。程序输出如下,
因此,在时间点1403324651641
和1403324651641
之间,最大的请求数是865次,这是Tomcat在这次运行中达到的峰值。
Shallow Heap(浅堆)表示该对象本身占用的内存大小,Retained Heap(深堆)表示该对象释放之后,可以释放的所有内存。因为一个对象可能引用别的对象,当该对象释放的时候,被引用的对象可能不再被引用了,因此也会被释放,所以一般有,Retained Heap>=Shallow Heap。具体Retained Heap会释放多少空间和JVM中的支配树概念有关,不再赘述。↩