使用jmap和MAT观察Java程序内存数据

使用jmap和MAT观察Java程序内存数据

背景

很多故障跟数据结构中实际存储的数值会很有关系。有时候我们能够预感到这类数据,例如:

  1. 某些 成员变量缓存池当前尺寸(size)、容积上限(capacity)
    为了加快服务响应速度,通常会把一些相对静态的内容在内存中做缓存。内存中容纳这些内容的容器称为缓存池缓存池已存对象的数目,即缓存池当前尺寸。这个当前尺寸与该缓存池实际占用内存量息息相关。
    由于内存有限,缓存池通常会设置容积上限。这个值可以控制缓存池 占用内存量的最大值。

  2. 某些类的对象数量、某个对象导致的无法回收内存量retained size

  3. 局部变量的值

有经验的开发人员可能将这些数值打印到日志中,便于故障分析。

但很多时候我们并没有预先打印这些数值,或者很计算出这些数值(如无法回收内存量)。在没有日志记录的情况下,该如何获得这些数值?

答案是使用jmap 和 MAT(Memory Analyzer Tool)!

工具介绍

  • jmap是JDK自带的命令行工具,用它可以将指定进程的整个Java堆(Java Heap)转储到指定的数据文件
  • MAT是一个第三方工具,可以从http://www.eclipse.org/mat/下载。必须在图形界面(GUI)中执行(例如Windows或Linux的VNC桌面系统中)。用它可以对数据文件做非常详尽的分析。

它们之间的关系图示如下:

工具使用

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)后,通常可以通过如下几个方式来查看:

  1. Leak Suspect: Memory Analyzer智能分析出的最可能发生内存泄漏的对象,通常也就是占用内存量(Retained Size)最大的对象
  2. Dominator Tree查看,常用的两种方式:
    • 按照占用内存量(Retained Size)从大到小排序(Dominator Tree的默认查看方式)
    • 正则表达式匹配类名,查看对应的类(Class)的对象(Object),或者对应类的静态成员(Static Member)
  3. Threads View查看:可以看到各个线程的调用栈,以及局部引用的值
  4. Object Query Language查询:可以用类似SQL的语句查询整个Java Heap堆

使用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级别相对)的数据内容。包括所有类、对象的值、引用关系、线程调用栈、局部引用,以及每个对象占用内存量等信息。而这些信息对故障追踪可能会起到非常大的帮助。


你可能感兴趣的:(jvm,mat)