用eclipse memory analyzer分析heapdump
目录
一、工具介绍
1.术语
Strong reference(强引用):就是我们new出来的对象,但是还是被持有的应用,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
soft reference(软引用):我们new出来的对象,但是已经不被具体对象持有,常见的就是缓存中的对象引用,垃圾回收在内存还够的时候不会回收该部分内存信息,只有在内存不够时才会回收这块引用的对象。软引用可用来实现内存敏感的高速缓存
weak reference(弱引用):当对象不在有强引用时候,垃圾回收时立刻回收
Phantom reference(虚引用):不会在内存中出现,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收
GC Roots:根元素。通常GC Roots是一个在当前线程的调用栈上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。
Shallow Size:
对象自身占用的内存大小,不包括它引用的对象。
针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。
针对数组类型的对象,它的大小是数组元素对象的大小总和。
Retained Size:
Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)
换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。
不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。
看图理解Retained Size
上图中,GC Roots直接引用了A和B两个对象。
A对象的Retained Size=A对象的Shallow Size
B对象的Retained Size=B对象的Shallow Size + C对象的Shallow Size
这里不包括D对象,因为D对象被GC Roots直接引用。
如果GC Roots不引用D对象呢?
此时, B对象的Retained Size=B对象的Shallow Size + C对象的Shallow Size + D对象的Shallow Size
2.实例
2.1导入一个内存文件后,用MAT打开,具体如下图所表示
2.2 details:一些基本信息
Size: 32.3 MB
Classes: 8.9k
Objects: 686.4k
Class Loader(类加载器BootstrapClassLoader、Extension ClassLoader、ApplicationClassLoader): 250 Unreachable Objects Histogram
2.3 biggest object by retained size:显示在内存较大的对象信息
list objects -- with outgoing references : 查看这个对象持有的外部对象引用。
list objects -- with incoming references : 查看这个对象被哪些外部对象引用。
show objects by class -- with outgoing references :查看这个对象类型持有的外部对象引用
show objects by class -- with incoming references :查看这个对象类型被哪些外部对象引用
paths to gc root:显示不同类型引用(Strong,soft,weak )到跟节点的路径。
merge shorest path to gc root : 合并最短路径到root节点。
java basics:
-- classloader 该对象对应的classloader信息。
-- thread details :线程信息
-- thread stacks :线程堆栈
-- find String : 在这个对象中查询需要的字符串
-- group by : 根据某个字段统计出现的个数
java collections:这个暂时没研究。
leak Identification -- top consumers :几个大消耗内存的对象
2.4 可用操作
actions:列出每个类型的实例数及大小 。
Histogram :列出内存中的对象,对象的个数以及大小。
donimator tree :列出所有对象在整个内存对象中所占百分比。比较有用。
Top Consumers: 根据类名和包名列出开销最大的对象。
Duplicate Classes: 查找出在不同classloader中加载的相同类。
step by step 方式:
图 8显示的是应用程序中仍存在的所有异常,以及当异常抛出时所显示的消息。
提取与异常相关的额外信息
虽然从转储文件找到异常对象使您能够恢复异常消息,有时异常消息太过于普通或模糊,以致您无法理解问题的根源。其中一个很好的例子是java.net.ConnectException。如果尝试创建一个套接字连接来访问一个无法连接的主机时,您会得到下面的消息:
java.net.ConnectException: Connection refused: connect at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:352) at java.net.PlainSocketImpl.connectToAddress(PlainSocketImpl.java:214) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:201) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:377)
at java.net.Socket.connect(Socket.java:530) at java.net.Socket.connect(Socket.java:480) at java.net.Socket.(Socket.java:377) at java.net.Socket.(Socket.java:220) |
如果您有创建这个套接字的代码,并且您能够看到代码所使用的主机名和端口,那么这个消息就已经足够。在更复杂的代码中,其主机名和端口会经常变化,因为它们是从外部来源获取(用户输入值、数据库等),那么这个消息可能无法帮助您理解为什么连接被拒绝。
堆跟踪应该包括一个套接字对象,其中包含了所需要的数据,而如果我们能够使用 Memory Analyzer 从快照转储文件中查找这个套接字对象,然后我们就能够确定连接所拒绝的主机名和端口。
完成这个操作的最简单方法是在异常抛出后就生成一个转储文件。这在 IBM 运行时环境中可以使用以下-Xdump选项集实现:
-Xdump:system:events=throw,range=1..1, filter=java/net/ConnectException#java/net/PlainSocketImpl.socketConnect |
这个选项会在PlainSocketImpl.socketConnect()方法第一次出现ConnectException时生成一个 IBM 系统转储文件。
在将所生成的转储文件加载到 Memory Analyzer 之后,我们可以使用 Open Query Browser > Java Basics > Thread Stacks 选项列出线程跟踪中与每一个方法相关的线程和对象。
通过展开当前线程和线程中的方法帧,您就能够查看与这些方法相关的对象。在遇到java.net.ConnectException时,最应该关注的方法是java.net.Socket.connect()。展开这个方法帧会显示内存中一个java.net.Socket对象的引用。这是我们尝试创建的套接字连接。
当Socket对象选中时,它的域会显示在 Inspector 视图中,如图 9 所示:
图 9中的信息并不是非常有用,因为Socket的实际实现位于impl域。您可以通过下面两种方法来检查impl对象的内容,一是展开Socket对象,然后选择主面板中的impl java.net.SocksSocketImpl,二是右键单击 Inspector 视图中的impl域,然后选择 Go Into。现在 Inspector 视图会显示SocksSocketImpl的域,如图 10 所示:
图 10. SocksSocketImpl对象的 Inspector 视图
图 10所示的视图能够查看address和port域。在这里,端口是100,但是地址域指向一个java.net.Inet4Address对象。按照相同的过程来查看Inet4Address对象的域的结果如图 11 所示:
图 11. Inet4Address对象的 Inspector 视图
您会发现hostName被设置为baileyt60p。
技巧与方法
下面是一些有用的技巧和方法:
要注意 Memory Analyzer 本身可能会遇到内存耗尽问题。对于 Eclipse MAT,要编辑 MemoryAnalyzer.ini 文件中的-Xmx配置值。对于 ISA 版本,要编辑 ISA install/rcp/eclipse/plugins/com.ibm.rcp.j2se.../jvm.properties 文件。
如果您的 32 位 Memory Analyzer 仍然遇到内存耗尽问题,可以改用 64 位 Eclipse MAT 或尝试使用傻瓜(headless)模式(见参考资料)。(ISA 工具目前不支持 64 位平台。)
Memory Analyzer 会在转储文件的目录中写入“交换”文件,这会减少转储文件的重新加载时间。这些可以经过压缩,发送到另一台主机,然后保存到转储文件的相同目录中,这样就不需要重新加载完整的转储文件。
如果转储文件的大小在转储文件发生时与垃圾收集器不匹配,那么要查看 Overview 选项卡中的 Unreachable Objects Histogram 链接。Java 堆可能会有很多的垃圾(例如,如果很长时间之前创建的集合有一段时间未使用)需要 Memory Analyzer 删除。
如果两个对象A和B互相之间没有直接引用,但是都外部引用某个集合的对象C,那么C对象集合的 Retained Heap 将不会包含在A或B占用集合中,而是包含在A与B拥用者的占用集合中。在某些情况下,B可能会临时观察集合C,它实际上是A的派生对象。在这种情况下,您可以右键单击A ,然后选择 Java Basics > Customized Retained Set,并使用B的地址作为排除(-x)参数。
您可以一次加载多个转储文件,然后进行比较。打开较新转储文件的 Histogram,单击顶部的 Compare,然后选择基线转储文件。
当您浏览一个引用树时,一定要知道这些引用可以直接或间接地指回一个“父”引用,这样您可以输入一个浏览循环或环路(例如,链表)。一定要知道对象的地址。此外,一定要知道如果对象的类名之前加上关键词class,那么您浏览的是这个类的静态实例。
大多数视图中显示的String值最多可以有 1,024 个字符。如果您使用整个String,那么您可以右键单击该对象。然后选择 Copy > Save value to file。
大多数视图都有一个导出按钮,而大多数 HTML 结果都是在文件系统中创建的,所以数据可以导出共享或进行进一步的转换。与之相关的是,您可以使用组合键 Ctrl+C 将表格中选中的任意行以文本方式复制到您的剪贴板中。
结束语
正如 Eclipse.org 所描述的,Memory Analyzer 一开始是作为“一个帮助您查询内存泄漏和减少内存消耗的快速富特性 Java 堆分析器”而开发的。但是它的功能显然超越了所描述的范畴。除了具有分析“常见的”内存问题的作用,快照转储文件还可以作为其他判断问题技术的替代或补充,如跟踪技术和补丁技术。特别是对于 HPROF Dump 和 IBM 系统转储文件,Memory Analyzer 能够给您许多内存信息,如原始源代码使用的基本数据类型和域的名称。通过使用本文所介绍的各种视图,您可以查看遇到的问题,或者对它进行反向工程,包括总体覆盖问题和内存效率问题、Eclipse 包和类加载器的关系、线程数据使用率和堆栈结果局部变量、异常等。OQL 和 Memory Analyzer 插件模型也使您能够更容易地使用查询语言和编程方法来检查转储文件,这有助于实现常见分析的自动化。
二、heapdump实例分析
1、应用程序包准备(java应用)
import java.lang.ref.*; import java.util.*;
class Grocery { privatestaticfinalintSIZE = 100000; // 属性d使得每个Grocery对象占用较多内存,有800K左右 privatedouble[] d = newdouble[SIZE]; private String id;
public Grocery(String id) { this.id = id; }
public String toString() { returnid; }
publicvoid finalize() { System.out.println("Finalizing " + id); } }
publicclass References { privatestatic ReferenceQueue rq = new ReferenceQueue();
publicstaticvoid checkQueue() { Reference inq = rq.poll(); // 从队列中取出一个引用 if (inq != null) System.out.println("In queue: " + inq + " : " + inq.get()); }
publicstaticvoid main(String[] args) { finalint size = 10; // 创建10个Grocery对象以及10个软引用 Set sa = new HashSet(); for (int i = 0; i < size; i++) { SoftReference ref = new SoftReference(new Grocery("soft" + i), rq); System.out.println("Just created soft: " + ref.get()); sa.add(ref); } System.gc(); checkQueue(); System.out.println("---------------------------------------------------"); // 创建10个Grocery对象以及10个弱引用 Set wa = new HashSet(); for (int i = 0; i < size; i++) { WeakReference ref = new WeakReference(new Grocery ("weak " + i), rq); System.out.println("Just created weak: " + ref.get()); wa.add(ref); } System.gc(); checkQueue(); System.out.println("---------------------------------------------------"); // 创建10个Grocery对象以及10个虚引用 Set pa = new HashSet(); for (int i = 0; i < size; i++) { PhantomReference ref =new PhantomReference(new Grocery("Phantom " + i), rq); System.out.println("Just created Phantom: " + ref.get()); pa.add(ref); } System.gc(); checkQueue(); } } |
2、jstat连接环境准备
nohup jstatd -J-Djava.rmi.server.hostname=10.10.230.220 -J-Djava.security.policy=jstatd.all.policy -p 8888 & |
3、监控工具安装
1)JDK1.7
2)Eclipse Memory Analyzer
4、设置glassfish的集群JVM环境变量并重启集群
-Xloggc:/glassfish/gc.log -XX:PermSize=20m -XX:MaxPermSize=40m -Xmx64m -XX:HeapDumpPath=/glassfish/ -XX:+HeapDumpOnOutOfMemoryError |
手工生产hprof文件: jmap -dump:live,format=b,file=/glassfish/java_pid6622.hprof 22334 |
5、在glassfish中部署应用程序
6、查看日志检查部署失败原因
[#|2013-03-12T00:07:32.303+0800|INFO|glassfish3.1.2|ShoalLogger|_ThreadID=16;_ThreadName=Thread-2;|GMS1092: GMS View Change Received for group: dasc01 : Memb ers in view for IN_DOUBT_EVENT(before change analysis) are : ……………………………………………..
[#|2013-03-12T00:07:32.549+0800|WARNING|glassfish3.1.2|ShoalLogger|_ThreadID=15;_ThreadName=Thread-2;|PermGen space java.lang.OutOfMemoryError: PermGen space |
7、查看是否产生heapdump文件
8、jconsole查看permgen大小
9、下载用MAT分析heapdump文件
从下图初步分析是由于system class loader加载类时导致
10、通过dominator tree查看较大对象在整个内存对象中所占百分比
发现以下类占用比例较大
查看子类发现大量存在
11、通过java visualvm 打开heapdump也发现问题
详细信息如下:
12、结合源代码分析,应该是在testgc类load期间new大量对象导致溢出。
三、javacore实例分析
1、查找CPU资源占用较多的进程
[root@cc-ful2-01 ~]# top 或者 ps -eo user,pid,tid,pcpu -T|grep PID|sort -rn |head -50
top - 11:25:33 up 94 days, 8:59, 1 user, load average: 0.39, 0.36, 0.44
Tasks: 134 total, 1 running, 130 sleeping, 0 stopped, 3 zombie
Cpu(s): 1.0% us, 0.2% sy, 0.0% ni, 98.5% id, 0.2% wa, 0.0% hi, 0.0% si
Mem: 16631600k total, 14928504k used, 1703096k free, 441700k buffers
Swap: 4192924k total, 1152k used, 4191772k free, 6893988k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
31058 was2 15 0 747m 431m 32m S 6 2.7 86:45.18 java
3801 was2 22 0 1600m 1.5g 34m S 1 9.4 118:51.46 java
7839 was1 16 0 1440m 1.2g 33m S 1 7.5 25:45.03 java
2200 was2 17 0 898m 829m 33m S 0 5.1 135:25.05 java
2、strace 跟踪操作系统进程
strace -o a.strace -f -F -p 31058 -p 3801 -p 7839
查看是否有读写操作和SystemOut或者SystemErr等关键词
-f :除了跟踪当前进程外,还跟踪其子进程。
-o file :将输出信息写到文件file中,而不是显示到标准错误输出(stderr)。
-p pid :绑定到一个由pid对应的正在运行的进程。此参数常用来调试后台进程。
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪.
3、查找资源占用较多的子进程
[root@cc-ful2-01 ~]# ps -eo user,pid,tid,pcpu -T|grep PID|sort -rn|head -50
was1 1426 9345 0.0
was1 1426 9134 0.0
was1 1426 8963 0.0
was1 1426 8875 0.0
4、 生产javacore
#./jstack -l 23163 >javacore.20100226.152745.11570.txt
生成core dump,文件名称类似于 javacore.20100226.152745.11570.txt
5、select to_char('27537','xxxxxxx') from dual (词句需要在数据库环境执行,例如在PL/SQL工具下执行)
将第3步得到的TID 转换成16进制
6、用第5步得到的16进制数去 core dump里找到该子线程为何消耗CPU
关键字 native ID:
7、查找资料、联系业务使用的插件,确认故障线程所做操作由何原因导致