本文内容:
- 如何进行 heap dump
- MAT 的使用
- object 的 Incoming 与 Outgoing References
- object 的 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<Student> studentList = new ArrayList<>();
}
class Student {
}
public class HeapDumpTest {
public static void main(String[] args) throws InterruptedException {
List<School> 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)
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
运行命令: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<String> 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<byte[]> 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<Object> 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.<clinit>(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 MO
- class MO占据了大部分的内存,继续点开,发现它里面有一个 5,242,896 (5MB)的byte数组
结合代码进行分析,不难看出:
- while里面每一次循环都用一个新的ClassLoader来加载class MO,每个class MO本身有一个5MB的static字段
- while里面虽然创建了class MO的实例,但没有引用它,所以实例会被GC
- while里面创建了class Leak的实例,然后加入LinkedList,Leak实例被LinkedList引用了,所以不会被GC
- Leak实例引用了class Leak,所以class Leak不会被GC
- class Leak引用了ClassLoader,所以对应的ClassLoader不会被GC
- ClassLoader里面的classes字段引用了class MO,所以class MO不会被GC
选中 class MO,从右键菜单中选择 Path to GC Roots -> exclude weak references:
参考资料
- Different Ways to Capture Java Heap Dumps
- How to Get the Size of an Object in Java
- Eclipse MAT — Incoming, Outgoing References
- SHALLOW HEAP, RETAINED HEAP