Java heap dump及分析

Java heap dump及分析

本文内容:

如何进行 heap dumpMAT 的使用object 的 Incoming 与 Outgoing Referencesobject 的 Shallow Size 与 Retained Size 以及计算方法dump 分析(一般的OOM,同一Class被加载多次,ClassLoader泄漏导致的OOM)

运行时获取 heap dump

命令:jmap -dump:format=b,file=$fileName.hprof $PID 可以通过 man jmap 查看完整的介绍

准备脚本,生成 heap dump:

#!/bin/bash

jps -l | grep $1 | awk '{print $1}' | xargs jmap -dump:format=b,file=logs/$1.hprof

命令:./run $className

MAT(Memory Analyzer Tool) 下载

下载地址:https://www.eclipse.org/mat/

MAT 使用说明

样例代码:

package demo.heap;

import java.util.ArrayList;

import java.util.List;

class School {

final List studentList = new ArrayList<>();

}

class Student {

}

public class HeapDumpTest {

public static void main(String[] args) throws InterruptedException {

List schoolList = new ArrayList<>();

for (int i = 0; i < 3; ++i) {

School school = new School();

for (int j = 0; j < 5; ++j) {

school.studentList.add(new Student());

}

schoolList.add(school);

}

Thread.sleep(1000000000);

}

}

运行命令:./run HeapDumpTest,在logs目录下生成了HeapDumpTest.hprof文件。 使用MAT打开 HeapDumpTest.hprof:File -> Open Heap Dump… 点击 Actions->Histogram, 在 "Class Name"下方的搜索框输入类名:“School”,按回车,可以看到School class有3个Object。 选中"demo.heap.School"那一行,然后在右键菜单选择List objects -> with outgoing references 可以看到3个School objects,展开其中一个School object,可以看到它的studentList字段下有5个Student objects。

Incoming 与 Outgoing References

代码:

package demo.heap;

class A {

C c1 = C.getInstance();

}

class B {

C c2 = C.getInstance();

}

class C {

private static final C instance = new C();

private C() {

}

D d = new D();

E e = new E();

static C getInstance() {

return instance;

}

}

class D {

}

class E {

}

public class IncomingAndOutgoing {

public static void main(String[] args) throws InterruptedException {

A a = new A();

B b = new B();

Thread.sleep(1000000000);

}

}

代码生成的对象图: (图片来源:https://dzone.com/articles/eclipse-mat-incoming-outgoing-references)

对于A来说,C是它的Outgoing reference 对于来说,C是它的Outgoing reference 对C来说,A,B和 “Class C的instance” 是它的Incoming references;D,E和 “Class C” 是它的Outgoing references。 对于D来说,C是它的Incoming reference 对于E来说,C是它的Incoming reference

运行命令:./run IncomingAndOutgoing 在MAT中打开 IncomingAndOutgoing.hprof 文件 遵循之前的步骤:

打开 Histogram 视图输入 demo 进行搜索 得到以下结果: 选中 “demo.heap.C”,右键菜单 “List objects -> with outgoing references” 返回 Histogram tab页,选中 “demo.heap.C”,右键菜单 “List objects -> with incoming references”

Shallow Size 与 Retained Size

Shallow Size: 对象自身所占内存的大小 Retained Size: 对象被GC后,能释放的总大小(对象被GC时,会连带把只由它引用的其他对象一同回收)

样例代码:

package demo.heap;

class A1 {

byte[] bs = new byte[10];

B1 b1 = new B1();

C1 c1 = new C1();

}

class B1 {

byte[] bs = new byte[10];

D1 d1 = new D1();

E1 e1 = new E1();

}

class C1 {

byte[] bs = new byte[10];

F1 f1 = new F1();

G1 g1 = new G1();

}

class D1 {

byte[] bs = new byte[10];

}

class E1 {

byte[] bs = new byte[10];

}

class F1 {

byte[] bs = new byte[10];

}

class G1 {

byte[] bs = new byte[10];

}

public class ShallowAndRetainedSize {

public static void main(String[] args) throws InterruptedException {

A1 a1 = new A1();

Thread.sleep(1000000000);

}

}

代码改造的对象图: 查看它的heap dump 默认只显示了对象的Shallow Size,没有Retained Size,这是因为Retained Size需要计算,点击一下Calculate Retained Size按钮 上图是在JVM参数 -Xmx < 32G下的结果,如果改成 -Xmx32G (>=32G),那么结果会变成:

Shallow Size 和 Retained Size的计算

注意事项:

当前环境为 64bit OS;当前JVM的 -Xmx 设置小于32G,对象引用的大小均为4B(bytes);一旦 -Xmx >= 32G,对象引用的大小会变成8B;64bit OS下,每个对象占用的最小内存为16B,其中12B是头部,对象内存占用大小必须是 8B 的倍数,如果对象没有任何字段,则存在4B Padding。

下图是Shallow Size和Retained Size的计算过程:

Object Size的计算

需要使用 Java Instrumentation API,先建立一个java agent的jar

目录:

InstrumentUtils.java

package demo.instrument;

import java.lang.instrument.Instrumentation;

public class InstrumentUtils {

private static Instrumentation instrumentation;

public static void premain(String options, Instrumentation instrumentationArg) {

instrumentation = instrumentationArg;

}

public static Instrumentation getInstrumentation() {

return instrumentation;

}

}

MANIFEST.MF

Premain-Class: demo.instrument.InstrumentUtils

pom.xml (需要自行加入plugin version)

org.apache.maven.plugins

maven-jar-plugin

src/main/resources/META-INF/MANIFEST.MF

运行命令:mvn clean package -DskipTests 生成 instrument.jar。

测试代码: ObjectSizeCalculator.java

package demo.heap.size;

import demo.instrument.InstrumentUtils;

import java.lang.management.ManagementFactory;

import java.util.Optional;

class ObjectSizeCalculator {

static final String VM_XMX_ARG = "-Xmx";

static final int OBJECT_HEADER_SIZE = 12;

static final int SHORT_REF_SIZE = 4;

static final int LONG_REF_SIZE = 8;

static final int MULTIPLE_8 = 8;

static void printSize(String msg, Object o) {

System.out.println(msg + "Class " + o.getClass() + " Size: " + getSize(o) + "B.");

}

static long getSize(Object o) {

return InstrumentUtils.getInstrumentation().getObjectSize(o);

}

static Optional getXmx() {

return ManagementFactory.getRuntimeMXBean()

.getInputArguments()

.stream()

.filter(arg -> arg.startsWith(VM_XMX_ARG))

.findAny()

.map(arg -> {

System.out.println("Xmx: " + arg);

return arg;

});

}

}

例子1:观察padding

package demo.heap.size;

import static demo.heap.size.ObjectSizeCalculator.printSize;

public class ObjectSizeTest {

public static void main(String[] args) {

printSize("No fields: ", new T0());

printSize("1 byte field: ", new T1());

printSize("2 byte field: ", new T2());

printSize("3 byte field: ", new T3());

printSize("4 byte field: ", new T4());

printSize("5 byte field: ", new T5());

}

private static class T0 {

}

private static class T1 {

byte b;

}

private static class T2 {

byte b;

byte b2;

}

private static class T3 {

byte b;

byte b2;

byte b3;

}

private static class T4 {

byte b;

byte b2;

byte b3;

byte b4;

}

private static class T5 {

byte b;

byte b2;

byte b3;

byte b4;

byte b5;

}

}

运行时需要加入instrument.jar作为javaagent,同时-Xmx<32G:

-javaagent:/home/helowken/instrument-1.0.jar -Xmx31G

输出:

No fields: Class class demo.heap.size.ObjectSizeTest$T0 Size: 16B.

1 byte field: Class class demo.heap.size.ObjectSizeTest$T1 Size: 16B.

2 byte field: Class class demo.heap.size.ObjectSizeTest$T2 Size: 16B.

3 byte field: Class class demo.heap.size.ObjectSizeTest$T3 Size: 16B.

4 byte field: Class class demo.heap.size.ObjectSizeTest$T4 Size: 16B.

5 byte field: Class class demo.heap.size.ObjectSizeTest$T5 Size: 24B.

可以看出,在0~4个byte field的时候,object header(12B) + (0 ~ 4B) <= 16,当有5个byte field时,总和就到了17B(17 % 8 = 1),需要对齐到24B,padding为7。

例子2: 查看 byte数组(byte[])的大小

package demo.heap.size;

import java.text.MessageFormat;

import static demo.heap.size.ObjectSizeCalculator.*;

public class ByteArraySizeCalculator {

private static final String pattern1 = "byte[0] shallow size: class header(12B) + ref size({0}B) + padding({1}B) = {2}B.";

private static final String pattern2 = "byte[{0}] shallow size: {1}B + byte[0] Shallow Size({2}B) + padding({3}B) = {4}B.";

private static int getReferenceSize(String arg) {

// for simplicity, we just assume the format is -Xmx{N}G

int memory = Integer.parseInt(arg.substring(VM_XMX_ARG.length(), arg.length() - 1));

if (memory >= 32)

return LONG_REF_SIZE;

return SHORT_REF_SIZE;

}

private static int getShallowSize(int refSize) {

int shallowSize = OBJECT_HEADER_SIZE + refSize;

int remainder = shallowSize % MULTIPLE_8;

if (remainder > 0)

shallowSize += MULTIPLE_8 - remainder;

long padding = shallowSize - OBJECT_HEADER_SIZE - refSize;

System.out.println(MessageFormat.format(pattern1, refSize, padding, shallowSize));

return shallowSize;

}

private static void printBySizes(int byteArrayShallowSize) {

for (int i = 1; i <= 10; ++i) {

long size = getSize(new byte[i]);

long padding = size - byteArrayShallowSize - i;

System.out.println(MessageFormat.format(pattern2, i, i, byteArrayShallowSize, padding, size));

}

}

public static void main(String[] args) {

int refSize = getXmx()

.map(ByteArraySizeCalculator::getReferenceSize)

.orElse(SHORT_REF_SIZE);

System.out.println("Reference size: " + refSize + "B");

int shallowSize = getShallowSize(refSize);

printBySizes(shallowSize);

}

}

运行时需要加入instrument.jar作为javaagent,同时-Xmx<32G:

-javaagent:/home/helowken/instrument-1.0.jar -Xmx31G

输出:

Xmx: -Xmx31G

Reference size: 4B

byte[0] shallow size: class header(12B) + ref size(4B) + padding(0B) = 16B.

byte[1] shallow size: 1B + byte[0] Shallow Size(16B) + padding(7B) = 24B.

byte[2] shallow size: 2B + byte[0] Shallow Size(16B) + padding(6B) = 24B.

byte[3] shallow size: 3B + byte[0] Shallow Size(16B) + padding(5B) = 24B.

byte[4] shallow size: 4B + byte[0] Shallow Size(16B) + padding(4B) = 24B.

byte[5] shallow size: 5B + byte[0] Shallow Size(16B) + padding(3B) = 24B.

byte[6] shallow size: 6B + byte[0] Shallow Size(16B) + padding(2B) = 24B.

byte[7] shallow size: 7B + byte[0] Shallow Size(16B) + padding(1B) = 24B.

byte[8] shallow size: 8B + byte[0] Shallow Size(16B) + padding(0B) = 24B.

byte[9] shallow size: 9B + byte[0] Shallow Size(16B) + padding(7B) = 32B.

byte[10] shallow size: 10B + byte[0] Shallow Size(16B) + padding(6B) = 32B.

如果修改-Xmx >=32G,也就是 -Xmx32G后,输出:

Xmx: -Xmx32G

Reference size: 8B

byte[0] shallow size: class header(12B) + ref size(8B) + padding(4B) = 24B.

byte[1] shallow size: 1B + byte[0] Shallow Size(24B) + padding(7B) = 32B.

byte[2] shallow size: 2B + byte[0] Shallow Size(24B) + padding(6B) = 32B.

byte[3] shallow size: 3B + byte[0] Shallow Size(24B) + padding(5B) = 32B.

byte[4] shallow size: 4B + byte[0] Shallow Size(24B) + padding(4B) = 32B.

byte[5] shallow size: 5B + byte[0] Shallow Size(24B) + padding(3B) = 32B.

byte[6] shallow size: 6B + byte[0] Shallow Size(24B) + padding(2B) = 32B.

byte[7] shallow size: 7B + byte[0] Shallow Size(24B) + padding(1B) = 32B.

byte[8] shallow size: 8B + byte[0] Shallow Size(24B) + padding(0B) = 32B.

byte[9] shallow size: 9B + byte[0] Shallow Size(24B) + padding(7B) = 40B.

byte[10] shallow size: 10B + byte[0] Shallow Size(24B) + padding(6B) = 40B.

由此可以看出 byte数组大小的组成:object header(12B) + reference(4 or 8B) + sum(bytes)

到此,你应该不会再对上面的 Shallow Size 和 Retained Size 感到迷惑了。

OOM 后获取 heap dump

OOM代码:

package demo.heap.oom;

import java.util.LinkedList;

import java.util.List;

public class OOMTest {

private static final List bsList = new LinkedList<>();

public static void main(String[] args) {

int size = 1024 * 1024 * 10;

int count = 0;

while (true) {

bsList.add(new byte[size]);

System.out.println("Add 10M byte[]: " + ++count);

}

}

}

运行时需要加入以下参数,让JVM在OOM时生成 heap dump:

-Xmx32M

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/home/helowken/heap_dumps/logs/OOMTest.hprof

注意事项:

HeapDumpPath 当前用户必须有权限对其进行写操作如果 HeapDumpPath 已经有文件存在,dump会失败

运行程序,稍等一下,程序会OOM然后终止,输出:

Add 10M byte[]: 1

Add 10M byte[]: 2

java.lang.OutOfMemoryError: Java heap space

Dumping heap to /home/helowken/heap_dumps/logs/OOMTest.hprof ...

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at demo.heap.oom.OOMTest.main(OOMTest.java:13)

Heap dump file created [22029596 bytes in 0.039 secs]

使用MAT打开 heap dump,在向导界面中选择 Leak Suspects Report 打开后,MAT会自动生成问题报告,供我们参考: 从图中可以看出 java.util.LinkedList 的一个实例占用了 98.77% 的bytes。 点开 Details 连接,可以看到更信息的报告: 点击 “class OOMTest”,选择 List objects -> with outgoing references: 从图中可以看出 OOMTest 的 bsList 占用了 20M+ 的bytes。选中 bsList,从右键菜单中选择 Path To GC Roots -> exclude weak references: 可以看到 bsList 被引用着,所以它没法被 GC,最终因为没法分配更多内存而导致了OOM。

另外,还可以通过 dominator_tree 来直观地查看各个class占用的内存: 从图中可以看到 LinkedList 中的两个元素,分别指向10M的 byte数组。

更多有趣的例子

准备代码: MoClassLoader.java

package demo.heap.oom.classLoader;

import java.net.URL;

import java.net.URLClassLoader;

public class MoClassLoader extends URLClassLoader {

private final String loaderName;

MoClassLoader(String loaderName, URL[] urls) {

super(urls);

this.loaderName = loaderName;

}

@Override

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {

Class clazz = findLoadedClass(name);

if (clazz == null) {

try {

return findClass(name);

} catch (ClassNotFoundException e) {

return super.loadClass(name, resolve);

}

}

return clazz;

}

@Override

public String toString() {

return loaderName;

}

}

例子1: 同一个Class被多个ClassLoader加载

package demo.heap.oom.classLoader;

import java.net.URL;

public class DifferentClassLoader {

public static void main(String[] args) throws Exception {

MO mo = new MO();

URL url = MO.class.getProtectionDomain().getCodeSource().getLocation();

String className = MO.class.getName();

ClassLoader newLoader = new MoClassLoader("NewMoLoader1", new URL[]{url});

Class newMoClass = newLoader.loadClass(className);

Object newMO = newMoClass.newInstance();

ClassLoader newLoader2 = new MoClassLoader("NewMoLoader2", new URL[]{url});

Class newMoClass2 = newLoader2.loadClass(className);

Object newMO2 = newMoClass2.newInstance();

System.out.println("MO class: " + mo.getClass().getName() + ", loader: " + mo.getClass().getClassLoader());

System.out.println("newMO class: " + newMO.getClass().getName() + ", loader: " + newMO.getClass().getClassLoader());

System.out.println("new MO2 class: " + newMO2.getClass().getName() + ", loader: " + newMO2.getClass().getClassLoader());

Thread.sleep(1000000000);

}

public static class MO {

}

}

输出:

MO class: demo.heap.oom.classLoader.DifferentClassLoader$MO, loader: sun.misc.Launcher$AppClassLoader@18b4aac2

newMO class: demo.heap.oom.classLoader.DifferentClassLoader$MO, loader: NewMoLoader1

new MO2 class: demo.heap.oom.classLoader.DifferentClassLoader$MO, loader: NewMoLoader2

运行命令:./run DifferentClassLoader 生成 heap dump。 在MAT的 Histogram中选择 Group by class loader 可以看到 MO这个class被3个 ClassLoader所加载。

例子2:ClassLoader 泄漏导致 OOM

package demo.heap.oom.classLoader;

import java.net.URL;

import java.util.LinkedList;

import java.util.List;

public class LeakClassLoader {

public static void main(String[] args) throws Exception {

List leaks = new LinkedList<>();

URL url = MO.class.getProtectionDomain().getCodeSource().getLocation();

String moClassName = MO.class.getName();

String leakClassName = Leak.class.getName();

int count = 0;

while (true) {

ClassLoader newLoader = new MoClassLoader("NewMoLoader1", new URL[]{url});

Class newMoClass = newLoader.loadClass(moClassName);

newMoClass.newInstance();

Class newLeakClass = newLoader.loadClass(leakClassName);

leaks.add(newLeakClass.newInstance());

System.out.println("Add leak times: " + ++count);

}

}

public static class Leak {

}

public static class MO {

private static final byte[] bs = new byte[1024 * 1024 * 5];

}

}

运行时加入参数:

-Xmx32M

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/home/helowken/heap_dumps/logs/LeakClassLoader.hprof

输出:

Add leak times: 1

Add leak times: 2

Add leak times: 3

Add leak times: 4

Add leak times: 5

java.lang.OutOfMemoryError: Java heap space

Dumping heap to /home/helowken/heap_dumps/logs/LeakClassLoader.hprof ...

Heap dump file created [27496363 bytes in 0.060 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at demo.heap.oom.classLoader.LeakClassLoader$MO.(LeakClassLoader.java:32)

at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)

at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)

at java.lang.reflect.Constructor.newInstance(Constructor.java:423)

at java.lang.Class.newInstance(Class.java:442)

at demo.heap.oom.classLoader.LeakClassLoader.main(LeakClassLoader.java:20)

用MAT分析 dump,使用 Leak Suspects 是不是很奇怪,LinkedList 竟然占用了 98.96% 的内存,但我们的代码只是不断往里面添加 Leak 的实例,而 class Leak 是没有任何字段的,根据上面 Shallow Size的计算,Leak 的实例只有16B。

点击 LinkedList,选择 List objects -> with outgoing references: 从图中可以看出:

Leak 的实例确实只占用了 16B,但是Leak的ClassLoader确占据了大部分的内存一层层点开ClassLoader,可以看到ClassLoader下面的classes -> elementData里面存放了2个class,其中一个是class Leak,另外一个就是class MOclass MO占据了大部分的内存,继续点开,发现它里面有一个 5,242,896 (5MB)的byte数组

结合代码进行分析,不难看出:

while里面每一次循环都用一个新的ClassLoader来加载class MO,每个class MO本身有一个5MB的static字段while里面虽然创建了class MO的实例,但没有引用它,所以实例会被GCwhile里面创建了class Leak的实例,然后加入LinkedList,Leak实例被LinkedList引用了,所以不会被GCLeak实例引用了class Leak,所以class Leak不会被GCclass Leak引用了ClassLoader,所以对应的ClassLoader不会被GCClassLoader里面的classes字段引用了class MO,所以class MO不会被GC

选中 class MO,从右键菜单中选择 Path to GC Roots -> exclude weak references:

参考资料

Different Ways to Capture Java Heap DumpsHow to Get the Size of an Object in JavaEclipse MAT — Incoming, Outgoing ReferencesSHALLOW HEAP, RETAINED HEAP