使用jmap和MAT观察Java程序内存数据
很多故障跟数据结构中实际存储的数值会很有关系。有时候我们能够预感到这类数据,例如:
某些 成员变量:缓存池的当前尺寸(size)、容积上限(capacity)
为了加快服务响应速度,通常会把一些相对静态的内容在内存中做缓存。内存中容纳这些内容的容器称为缓存池。缓存池中已存对象的数目,即缓存池的当前尺寸。这个当前尺寸与该缓存池实际占用内存量息息相关。
由于内存有限,缓存池通常会设置容积上限。这个值可以控制缓存池 占用内存量的最大值。
某些类的对象数量、某个对象导致的无法回收内存量(retained size)
局部变量的值
有经验的开发人员可能将这些数值打印到日志中,便于故障分析。
但很多时候我们并没有预先打印这些数值,或者很计算出这些数值(如无法回收内存量)。在没有日志记录的情况下,该如何获得这些数值?
答案是使用jmap 和 MAT(Memory Analyzer Tool)!
它们之间的关系图示如下:
jmap使用方法:
jmap -dump:file=dump.map <pid>
执行后,会生成dump.map文件。这个文件可以用MAT打开。
如果你在远程Linux系统中工作,可能需要使用VNC(具体方法参考: (TODO) )。
启动MAT的方法(在Linux桌面中,打开一个终端(Terminal),然后执行):
#假设mat工具已经解压到/usr/local/mat中 cd /usr/local/mat #如果dump.map文件尺寸较大(例如2GB以上),需要修改MemoryAnalyzer.ini文件中 #-Xmx选项。将它设置到一个更大的值,例如4GB左右: -Xmx4000m #启动Memory Analyzer ./MemoryAnalyzer
启动后,在菜单中选择Open Heap Dump,读入dump.map文件即可开始分析。
jmap生成的数据文件(例如dump.map)中有详尽的Java堆(Java Heap)的信息,这些信息包括—所有的Java对象的数据成员,以及它们之间的引用关系等。并且MAT又是一个非常棒的工具,因此上述内容都可以看到。并且MAT还为内存泄漏的分析有特别的支持,它能够计算出每个对象的占用内存量(Retained Size)。将对象按照占用内存量(Retained Size)从大到小排序,通常很容易就能确定究竟是什么对象发生了内存泄漏。
在MAT中打开数据文件(例如dump.map)后,通常可以通过如下几个方式来查看:
使用MAT不但可以观察(Inspect)所有对象的成员变量的值,如果变量是引用,还可以跟踪(follow)引用(reference)进一步查看相关对象的数据。
撰写程序如下(MatExample.java):
import java.util.List; import java.util.LinkedList; public class MatExample { static String staticVar = "Hello, I am static"; private String instanceVar = "Hello, I am instance"; public static void main(String[] args) throws Exception { MatExample instance = new MatExample(); List<String> leakList = new LinkedList<String>(); int k = 0; for(int i=0; i<10000; i++) { k++; leakList.add(k+""); } System.out.println("10000 objects generated"); while(true) { Thread.sleep(1000); } } }
编译(生成MatExample.class)
javac MatExample.java
运行(MatExample.class)
java MatExample
程序输出:
1000 objects generated
另开一个命令行,然后使用jps列出所有Java进程:
jps -mlvV
jps输出类似如下信息:
91888 sun.tools.jps.Jps -mlvV -Dapplication.home=E:\Java\jdk1.7.0_45 -Xms8m 90752 MatExample
上面90752便是MatExample的进程编号(PID),现在我们运行jmap获取数据文件(存储到dump.map):
jmap -dump:file=dump.map 90752
接着启动MAT,并通过菜单中 File->Open Heap Dump 打开dump.map,如图:
MAT会读取数据文件到内存中并作必要的预处理(如果是较大的文件,则耗时较长,建议去喝杯茶休息下)。
MAT读取完文件,会出现一个向导,让我们选择一种分析方式。我们可以选择泄漏嫌疑对象报告(Leak Suspects Report)分析,然后点“Finish”。
MAT呈现出一个饼图描述该进程内存的分布情况,在此图形中可以清晰地看到内存占用最大的对象。下方,还会有Top N的泄露嫌疑对象文字描述,如图:
这里说的是局部变量占用“721,136 (63.90%) bytes”,并且是一个LinkedList对象造成的。点击“See stacktrace”,就能看到调用栈
main at java.lang.Thread.sleep(J)V (Native Method) at MatExample.main([Ljava/lang/String;)V (MatExample.java:21)
线程名称是“main”。点击图示的按钮,可以打开线程视图(Threads view),查看带有局部变量的调用栈信息。
从中可以清晰看到是在MatExample.main函数中引用的一个LinkedList占用了720,040字节。
我们可以点击菜单中Window -> Inspector,然后查看该LinkedList对象的属性。
可见,其size属性确实是10000。可见泄露嫌疑对象报告(Leak Suspect Report)能够帮助我们较迅速地定位到内存泄漏点。
另外,也可以从内存占用最大的对象的思路来找内存问题,点击图示图标,打开Dominator Tree View
将最上面的一行逐行展开,也可以看到LinkedList的内存占用较大。
下面尝试查看指定类型(class)的静态成员(static member)和对象
在Dominator Tree的正则输入框(Regex)中输入“MatExample”:
回车后,显示下图:
“class MatExample”的属性和引用,与代码中定义的MatExample类的静态成员(static member)的内容相符合:
static String staticVar = "Hello, I am static";
我们可以尝试下跟踪引用:在Inspector窗口中,对一个引用点击右键,然后选择“Go Into”
即可看到该对象的属性。
如果希望让引用对象显示在右图中,可以选择"List objects -> with outgoing references”
这样可以更方便地进一步跟踪其引用。
但我们发现,代码中创建的MatExample对象
MatExample instance = new MatExample();
并没有出现在之前的图中
可以尝试下Object Query Language (OQL)。点击图标
输入查询语句
select * from INSTANCEOF MatExample
点击执行按钮
现在我们可以查到所有MatExample对象(本例只有一个):
点选后,在Inspector窗口中,同样可以看到其成员变量的值。这里就不详述了。
jmap和MAT是一对非常强大的工具,通过它们可以获知一个Java进程中所有Java级别(与Native级别相对)的数据内容。包括所有类、对象的值、引用关系、线程调用栈、局部引用,以及每个对象占用内存量等信息。而这些信息对故障追踪可能会起到非常大的帮助。