Java heap dump及分析

本文内容:

  1. 如何进行 heap dump
  2. MAT 的使用
  3. object 的 Incoming 与 Outgoing References
  4. object 的 Shallow Size 与 Retained Size 以及计算方法
  5. 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…
Java heap dump及分析_第1张图片
点击 Actions->Histogram, 在 "Class Name"下方的搜索框输入类名:“School”,按回车,可以看到School class有3个Object。
Java heap dump及分析_第2张图片
选中"demo.heap.School"那一行,然后在右键菜单选择List objects -> with outgoing references
Java heap dump及分析_第3张图片
可以看到3个School objects,展开其中一个School object,可以看到它的studentList字段下有5个Student objects。
Java heap dump及分析_第4张图片


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);
    }
}

代码生成的对象图:
Java heap dump及分析_第5张图片(图片来源: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 文件
遵循之前的步骤:

  1. 打开 Histogram 视图
  2. 输入 demo 进行搜索
    得到以下结果:
    Java heap dump及分析_第6张图片
    选中 “demo.heap.C”,右键菜单 “List objects -> with outgoing references”
    Java heap dump及分析_第7张图片
    返回 Histogram tab页,选中 “demo.heap.C”,右键菜单 “List objects -> with incoming references”
    Java heap dump及分析_第8张图片

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);
    }
}

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

Shallow Size 和 Retained Size的计算

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

下图是Shallow SizeRetained Size的计算过程:
Java heap dump及分析_第13张图片

Object Size的计算

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

目录
Java heap dump及分析_第14张图片

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
注意事项:
  1. HeapDumpPath 当前用户必须有权限对其进行写操作
  2. 如果 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
Java heap dump及分析_第15张图片
打开后,MAT会自动生成问题报告,供我们参考:
Java heap dump及分析_第16张图片
从图中可以看出 java.util.LinkedList 的一个实例占用了 98.77% 的bytes。
点开 Details 连接,可以看到更信息的报告:
Java heap dump及分析_第17张图片
点击 “class OOMTest”,选择 List objects -> with outgoing references
Java heap dump及分析_第18张图片
从图中可以看出 OOMTest 的 bsList 占用了 20M+ 的bytes。选中 bsList,从右键菜单中选择 Path To GC Roots -> exclude weak references
Java heap dump及分析_第19张图片
可以看到 bsList 被引用着,所以它没法被 GC,最终因为没法分配更多内存而导致了OOM。

另外,还可以通过 dominator_tree 来直观地查看各个class占用的内存:
Java heap dump及分析_第20张图片
从图中可以看到 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
Java heap dump及分析_第21张图片
可以看到 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
Java heap dump及分析_第22张图片
是不是很奇怪,LinkedList 竟然占用了 98.96% 的内存,但我们的代码只是不断往里面添加 Leak 的实例,而 class Leak 是没有任何字段的,根据上面 Shallow Size的计算,Leak 的实例只有16B。

点击 LinkedList,选择 List objects -> with outgoing references:
Java heap dump及分析_第23张图片
从图中可以看出:

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

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

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

选中 class MO,从右键菜单中选择 Path to GC Roots -> exclude weak references:
Java heap dump及分析_第24张图片

参考资料

  1. Different Ways to Capture Java Heap Dumps
  2. How to Get the Size of an Object in Java
  3. Eclipse MAT — Incoming, Outgoing References
  4. SHALLOW HEAP, RETAINED HEAP

你可能感兴趣的:(java)