命令: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
下载地址:https://www.eclipse.org/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。
代码:
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 文件
遵循之前的步骤:
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的计算过程:
需要使用 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;
});
}
}
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。
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代码:
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
运行程序,稍等一下,程序会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;
}
}
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所加载。
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
运行时加入参数:
-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:
从图中可以看出:
结合代码进行分析,不难看出:
选中 class MO,从右键菜单中选择 Path to GC Roots -> exclude weak references: