前言:

很多时候,我们需要分析下当前进程的内存使用情况(OOM等异常)。

一般,我们会先使用命令打印出一个heap dump文件(文件会比较大),然后使用命令或工具来分析占用内存比较大的对象。

命令的话就是使用本文中要介绍的jhat命令,而关于工具一般使用MAT工具(下一篇文章中介绍)。

本次使用jhat工具主要完成三个小目标:

* 占用内存最大对象分析

* 对象具体内容分析

* OQL语句使用

1.准备工作

1.1 基本对象

public class Student implements Serializable {
    private static final long serialVersionUID = -5423067787593230604L;

    private Long id;
    private String name;
    private int age;
    ...
}

public class User implements Serializable {
    private static final long serialVersionUID = -1754313838977971228L;

    private String password;
    private String username;
    private byte[] bytes = null;
    public User(String password, String username) {
        this.password = password;
        this.username = username;

        // 在这里创建一个10M的数组
        bytes = new byte[10 * 1024 * 1024];
    }

	...
}

1.2 创建对象添加到集合

@Controller
@RequestMapping("/jvmtest")
public class JvmTest {
    // 模拟占用大内存
    private List<Byte[]> list = new ArrayList<>();
    
    // 以下三种模式集合信息
    private List<Student> stuList = new ArrayList<>();
    private Set<User> userSet = new HashSet<>();
    private Map<Long,Student> stuMap = new HashMap<>();
    
    @RequestMapping(path = "/addObj", method = RequestMethod.GET)
    public String addObj() {
        int i = 0;
        for (int j = 0; j < 10; j++) {
            userSet.add(new User("jack" + i, "pass" +i));

            Student student = new Student(Long.valueOf(i + 1), "lucy" + i, i);
            stuList.add(student);

            stuMap.put(student.getId(), student);
        }

        return "success";
    }

	@RequestMapping(path = "/full", method = RequestMethod.GET)
    public String full_test() {
        Byte[] array2 = new Byte[2 * 1024 * 1024];
        list.add(array2);

        return "success";
    }
}

多调用/jvmtest/full 请求,不断添加Byte[]到集合中,用于占用内存;

调用一次/jvmtest/addObj 请求,添加10个Student、10个User对象到各集合中;

1.3 产生heapdump文件

# 获取当前进程ID 为19832
PS D:\Program Files\Java\jdk1.8.0_131\bin> jps
3056
7648 Launcher
1828 RemoteMavenServer
19832 Bootstrap
19772 Jps

# 使用jmap命令产生当前进程的heapdump文件,命名为heapdump.phrof
PS D:\Program Files\Java\jdk1.8.0_131\bin> .\jmap.exe  -dump:format=b,file=heapdump.phrof 19832
Dumping heap to D:\Program Files\Java\jdk1.8.0_131\bin\heapdump.phrof ...
Heap dump file created

2.jhat命令分析heapdump文件

2.1 jhat命令产生进程heapdump文件

# jhat 后面添加-J(可配置jvm参数,避免heapdump文件占用过大内存)
PS D:\Program Files\Java\jdk1.8.0_131\bin> .\jhat.exe -J-mx512M .\heapdump.phrof
Reading from .\heapdump.phrof...
Dump file created Mon Mar 21 11:45:07 GMT+08:00 2022
Snapshot read, resolving...
Resolving 261348 objects...
Chasing references, expect 52 dots....................................................
Eliminating duplicate references....................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

启动一个端口为7000的http服务,我们直接来访问来地址

2.2 查看HTML

通过访问http://localhost:7000

我们先来整体看下各个部分的含义

2.2.1 All Classes (excluding platform)

展示所有类信息,不包含平台相关类信息

All Classes (excluding platform)   						# 这里展示的所有的class信息
Package <Arrays>              							# 这里的类所在包名
class [Ljavax.servlet.DispatcherType; [0xc02ac410]		# 这里是具体类信息
class [Ljavax.servlet.FilterConfig; [0xc0811490]
class [Ljavax.servlet.SessionTrackingMode; [0xc0552d40]
class [Ljavax.servlet.jsp.JspContext; [0xfe133108]
class [Ljavax.servlet.jsp.JspWriter; [0xfe133038]
class [Ljavax.servlet.jsp.PageContext; [0xfe1330a0]
class [Ljavax.servlet.jsp.tagext.BodyContent; [0xfe132fd0]
class [Ljavax.servlet.jsp.tagext.VariableInfo; [0xc0811358]
class [Ljavax.websocket.CloseReason$CloseCode; [0xc0811b78]
class [Ljavax.websocket.CloseReason$CloseCodes; [0xc0811b10]

来看下我们的com.example.domain.User类信息

All Classes (excluding platform)
...
Package com.example.domain

class com.example.domain.Student [0xedeaa128] 
class com.example.domain.User [0xedea5ce0] # 类信息在这里,唯一的一个类

2.2.2 All Classes (including platform)

展示所有类信息,包含平台相关类信息

All Classes (including platform)
Package <Arrays>
class [B [0xc001a530]
class [C [0xc001ba98]
class [D [0xc0884e78]
class [F [0xc0884ee0]
class [I [0xc0049c40]
class [J [0xc07c23e0]
class [Lcom.sun.beans.util.Cache$CacheEntry; [0xfe17b0b8]
class [Lcom.sun.beans.util.Cache$Kind; [0xfe17b188]
class [Lcom.sun.jmx.mbeanserver.ClassLoaderRepositorySupport$LoaderEntry; [0xc08765c8]
class [Lcom.sun.jmx.mbeanserver.MXBeanMapping; [0xc0874150]
class [Lcom.sun.jmx.remote.security.MBeanServerFileAccessController$AccessType; [0xc0858d08]
...

与2.2.1基本没有啥区别

2.2.3 All Members of the Rootset

从根集能引用到的类(笔者也没太明白啥意思)

All Members of the Rootset

Java Static References
Static reference from com.sun.beans.TypeResolver.CACHE (from class com.sun.beans.TypeResolver) :
--> com.sun.beans.WeakCache@0xc0ef73d0 (24 bytes)
Static reference from com.sun.beans.finder.BeanInfoFinder.DEFAULT (from class com.sun.beans.finder.BeanInfoFinder) :
--> sun.beans.infos (28 bytes)
Static reference from com.sun.beans.finder.BeanInfoFinder.DEFAULT_NEW (from class com.sun.beans.finder.BeanInfoFinder) :
--> com.sun.beans.infos (28 bytes)
...

2.2.4 Instance Counts for All Classes (including platform)

这里是所有类的实例信息

Instance Counts for All Classes (including platform)

54597 instances of class [C   					
50177 instances of class java.lang.String		# 50177代表当前实例的数量
14590 instances of class java.util.HashMap$Node
11517 instances of class java.util.concurrent.ConcurrentHashMap$Node
9611 instances of class java.lang.reflect.Method
8234 instances of class [Ljava.lang.Class;
7504 instances of class java.lang.Object
6642 instances of class [Ljava.lang.Object;
5800 instances of class java.lang.Class
4715 instances of class [B

这里按照实例数量倒排序。

我们自行创建的User实例也在其中展示

...
10 instances of class com.example.domain.Student
10 instances of class com.example.domain.User

2.2.5 Heap Histogram

对象占用堆内存 柱状图

java 分析dump java 分析大 heapdump_Java

通过这个可以比较好的分析出占用内存最大的对象信息。

可以看到Byte占用内存最大。点击进去可以看到具体的byte对象信息

java 分析dump java 分析大 heapdump_java 分析dump_02

我们反过来通过User对象来验证下byte[]是否在上述references里

java 分析dump java 分析大 heapdump_java 分析dump_03

点击进去,

java 分析dump java 分析大 heapdump_java 分析dump_04

我们创建的10个User对象,已经在这,随便点击一个进去

java 分析dump java 分析大 heapdump_java_05

可以很清楚看到,当前User对象有三个属性,byte[],password=jack9,username=pass9

而数组对象0xf868bd98(占用内存为10M),在上述byte[]对象(占用内存最大的块里也可以找到)

java 分析dump java 分析大 heapdump_User_06

3.OQL使用

OQL是jhat支持的一种语言,类似于SQL,可以对堆内存中的对象进行查询。

语法还是有点复杂的,具体大家可以参考下别人的文档。

4.奇怪的知识点

上面在展示heap 的时候,[B 代表什么呢?关于这些标志的全解释如下:

[Z = boolean
[B = byte
[S = short
[I = int
[J = long
[F = float
[D = double
[C = char
[L = any non-primitives(Object)