文章目录
- JVM内存布局
- 对象头
- 实例数据
- 对齐填充
- 计算实例
- 数组占用内存大小
- String占用内存大小
- 对象占用内存计算
- 使用jmap的方式查看
- Instrumentation计算对象内存方式
- MAT内存分析示例
JVM内存布局
一个对象主要包含下面3个部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头
32位jvm对象头:8字节
64位jvm对象头:
- 16字节(不开启指针压缩情况,-XX:-UseCompressedOops)
- 12字节(开启指针压缩,-XX:+UseCompressedOops)
数组对象对象头:
- 24字节(不开启指针压缩情况,-XX:-UseCompressedOops)
- 16字节(开启指针压缩,-XX:+UseCompressedOops)
对象头也包含2部分数据:
- MarkWord:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- KlassPointer:指向它的类元数据的指针,JVM通过该指针来确定这个对象是哪个类的实例
实例数据
数据类型 | 占用字节 |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
引用类型32位JVM | 4 |
引用类型64位JVM | 8 |
引用类型64位JVM开启指针压缩 | 4 |
对齐填充
JVM按8字节对齐,不足8字节要填充(Padding)到8字节的整数倍。
填充字节数很容易计算:padding = 8 - ((对象头 + 实例数据) % 8)
计算实例
下面的计算实例都是在64位JVM开启指针压缩下的情况。
怎样计算对象占用内存大小呢?后面介绍2中方式:
- 使用Java8的jdk.nashorn.internal.ir.debug.ObjectSizeCalculator类的getObjectSize方法
- 使用ava.lang.instrument.Instrumentation类的getObjectSize方法(不能直接调用,得使用-javaagent方式,Instrumentation有JVM注入)
数组占用内存大小
数组计算:
MarkWord + KlassPointer + 数组长度 + 实例数据(数组长度*数组元数据大小) + 补齐填充
- 开启指针压缩:MarkWord(8字节) + KlassPointer(4字节) + 数组长度(4字节) + 0*4 + 补齐 0 = 16
- 关闭指针压缩:MarkWord(8字节) + KlassPointer(8字节) + 数组长度(4字节) + 0*8 + 补齐 0 = 24
默认是开启指针压缩,可以通过下面参数来关闭:
# 关闭指针压缩
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
# 查看最终生效的参数
-XX:+PrintFlagsFinal
# 查看默认的初始参数
-XX:+PrintFlagsInitial
相信,有亲子动手试过的朋友会发现,Java8的ObjectSizeCalculator类的getObjectSize方法计算出来的值都是和开启指针压缩一致。
刚开始,我以为关闭指针压缩没有生效,找了半天原因,最后使用Instrumentation的getObjectSize能对上,参数生效了,应该是ObjectSizeCalculator的问题。
Instrumentation方式稍微麻烦一些,放在后面介绍。
@Test
public void memoryArray() {
int[] base = new int[0];
System.out.println("int[0]占用内存大小:" + ObjectSizeCalculator.getObjectSize(base));
int[] a = new int[1];
System.out.println("int[1]占用内存大小:" + ObjectSizeCalculator.getObjectSize(a));
int[] b = new int[2];
System.out.println("int[2]占用内存大小:" + ObjectSizeCalculator.getObjectSize(b));
int[] c = new int[3];
System.out.println("int[3]占用内存大小:" + ObjectSizeCalculator.getObjectSize(c));
Integer[] d = new Integer[2];
System.out.println("Integer[2]占用内存大小:" + ObjectSizeCalculator.getObjectSize(d));
Integer[] e = new Integer[3];
System.out.println("Integer[3]占用内存大小:" + ObjectSizeCalculator.getObjectSize(e));
}
结果如下:
int[0]占用内存大小:16
int[1]占用内存大小:24
int[2]占用内存大小:24
int[3]占用内存大小:32
Integer[2]占用内存大小:24
Integer[3]占用内存大小:32
计算逻辑如下:
int[1] = 数组对象头16字节 + 1个int类型4字节 + 4个字节的padding = 24字节
int[2] = 数组对象头16字节 + 2个int类型4字节(8字节) = 24字节
int[3] = 数组对象头16字节 + 3个int类型4字节(12字节) + 4个字节的padding = 32字节
我们再看一个容易出错的char数组:
public static void memoryCharArray() {
char[] base = new char[0];
System.out.println("int[0]占用内存大小:" + ObjectSizeCalculator.getObjectSize(base));
char[] a = new char[1];
System.out.println("int[1]占用内存大小:" + ObjectSizeCalculator.getObjectSize(a));
char[] b = new char[2];
System.out.println("int[2]占用内存大小:" + ObjectSizeCalculator.getObjectSize(b));
char[] c = new char[4];
System.out.println("int[4]占用内存大小:" + ObjectSizeCalculator.getObjectSize(c));
char[] d = new char[5];
System.out.println("int[5]占用内存大小:" + ObjectSizeCalculator.getObjectSize(d));
}
int[0]占用内存大小:16
int[1]占用内存大小:24
int[2]占用内存大小:24
int[4]占用内存大小:24
int[5]占用内存大小:32
注意:在Java中char占用2个字节,而不是1个字节(从\u0000到\uffff)
计算逻辑如下:
int[1] = 数组对象头16字节 + 1个char类型2字节 + 6个字节的padding = 24字节
int[4] = 数组对象头16字节 + 4个char类型2字节(8字节) = 24字节
int[5] = 数组对象头16字节 + 5个char类型2字节(10字节) + 6个字节的padding = 32字节
String占用内存大小
public static void memoryString() {
System.out.println("new String()占用字节:" + ObjectSizeCalculator.getObjectSize(new String()));
System.out.println("new String(\"a\")占用字节:" + ObjectSizeCalculator.getObjectSize(new String("a")));
System.out.println("a占用字节:" + ObjectSizeCalculator.getObjectSize("a"));
System.out.println("ab占用字节:" + ObjectSizeCalculator.getObjectSize("ab"));
System.out.println("abc占用字节:" + ObjectSizeCalculator.getObjectSize("abc"));
System.out.println("abcd占用字节:" + ObjectSizeCalculator.getObjectSize("abcd"));
System.out.println("abcde占用字节:" + ObjectSizeCalculator.getObjectSize("abcde"));
}
结果如下:
new String()占用字节:40
new String("a")占用字节:48
a占用字节:48
ab占用字节:48
abc占用字节:48
abcd占用字节:48
abcde占用字节:56
空字符串的时候,String占用内存大小为40字节,怎么算的呢?
String有2个非static的成员变量:
-
private final char value[]
-
private int hash
-
value内存大小 = 对象头16字节 + 对齐0 = 16字节
-
String内存大小 = value内存大小16字节 + 对象头12字节 + int 4字节(hash) + 数组value引用4字节 + 对齐4字节= 40字节
“abcd”:
- value内存大小 = 对象头16字节 + 数组长度4 * 2字节 = 24字节
- String内存大小 = value内存大小24字节 + 对象头16字节 + int 4字节 + 数组value引用4字节 = 48字节
“abcde”:
- value内存大小 = 对象头16字节 + 数组长度5 * 2字节 + 6字节padding= 32字节
- String内存大小 = value内存大小32字节 + 对象头16字节 + int 4字节 + 数组value引用4字节 = 56字节
对象占用内存计算
public static void memoryCustomObject(){
System.out.println("MemoryCustomObject占用字节数:"
+ ObjectSizeCalculator.getObjectSize(new MemoryCustomObject()));
}
private static class MemoryCustomObject {
String s = new String();
int i = 0;
}
计算逻辑如下:
前面我们知道空字符串占用40字节 + s引用4字节 + i变量4字节 + MemoryCustomObject对象都12字节 = 60字节
60字节不是8的倍数,再加4字节padding,最终64字节。
如果我们把MemoryCustomObject换成下面这样,结果是多少呢?
private static class MemoryCustomObject {
String s;
int i = 0;
}
答案是24字节。
计算逻辑如下:
MemoryCustomObject对象头12字节 + s引用4字节 + i变量4字节 = 20字节 + 4字节padding = 24字节
使用jmap的方式查看
jps
jps -l
jmap -histo 6380
jmap -histo 6380 | findstr "MemoryCustomObject"
jmap -histo输出说明:
- #instances表示实例数量
- #bytes实例总的字节数
- class name实例的类名
数据类型 | class name |
---|---|
byte | B |
char | C |
int | I |
long | J |
boolean | Z |
double | D |
float | F |
short | S |
类接口 | 全限定名,例如java.lang.Class |
类接口数组 | Lclassname;例如[Ljava.lang.Class; |
jmap的class name中[表示数组,[[表示二维数组,以此类推。
例如[B就表示一维byte数组
我们可以看到100个MemoryCustomObject对象总大小是3200字节,每个32字节,为什么和我们前面计算的对不上呢?
还是因为我们添加了-XX:-UseCompressedClassPointers -XX:-UseCompressedOops参数,关闭了指针压缩,ObjectSizeCalculator计算得还是开启指针压缩的值。
还是要用我们后面介绍的Instrumentation方式MemoryPreMain.sizeOf(new MemoryCustomObject())才能计算关闭指针压缩的值。
Instrumentation计算对象内存方式
首先,不知道怎么打javaagent包的可以参考:javaagent与attach
用后面的MemoryPreMain类创建一个maven工程,打一个jar包,在测试Main工程中引用这个包,并且指定运行vm参数:
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops -XX:+PrintFlagsFinal -javaagent:E:\app\me\learn\agent-learn\target\agent-learn-1.0.0-jar-with-dependencies.jar
上面参数是关闭指针压缩,和设置-javaagent。
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Stack;
public class MemoryPreMain {
private static Instrumentation instrumentation;
/**
*
* @param agentArgs 由–javaagent传入
* @param instrumentationParam 由JVM传入
*/
public static void premain(String agentArgs, Instrumentation instrumentationParam) {
instrumentation = instrumentationParam;
}
/**
* 计算Shallow Size(只计算引用大小,不计算引用对象实际大小)
* @param toCalcObject 要计算内存占用大小的对象
* @return 对象Shallow Size
*/
public static long sizeOf(Object toCalcObject) {
if (instrumentation == null) {
throw new IllegalStateException("请指定-javaagent");
}
return instrumentation.getObjectSize(toCalcObject);
}
/**
*
* 计算Retained Size,计算对象及其引用对象大小
* @param toCalcObject 要计算内存占用大小的对象
* @return 对象Retained Size
*/
public static long fullSizeOf(Object toCalcObject) {
Map<Object, Object> visited = new IdentityHashMap<>();
Stack<Object> stack = new Stack<>();
long result = internalSizeOf(toCalcObject, stack, visited);
while (!stack.isEmpty()) {
result += internalSizeOf(stack.pop(), stack, visited);
}
visited.clear();
return result;
}
// 这个算法使每一个对象仅被计算一次。 避免循环引用,即死循环计算
private static boolean skipObject(Object obj, Map<Object, Object> visited) {
if (obj instanceof String) {
// String 池里已有的不再计算
if (obj == ((String) obj).intern()) {
return true;
}
}
return (obj == null) // 已有对象不再计算
|| visited.containsKey(obj);
}
private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) {
if (skipObject(obj, visited)){
return 0;
}
visited.put(obj, null);
long result = 0;
result += sizeOf(obj);
// 处理全部数组内容
Class clazz = obj.getClass();
if (clazz.isArray()) {
// [I , [F 基本类型名字长度是2,跳过基本类型数组
if(clazz.getName().length() != 2) {
int length = Array.getLength(obj);
for (int i = 0; i < length; i++) {
stack.add(Array.get(obj, i));
}
}
return result;
}
// 处理对象的全部字段
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 不反复计算静态类型字段
if (!Modifier.isStatic(field.getModifiers())) {
// 不反复计算原始类型字段
if (field.getType().isPrimitive()) {
continue;
} else {
// 使 private 属性可訪问
field.setAccessible(true);
try {
Object objectToAdd = field.get(obj);
if (objectToAdd != null) {
stack.add(objectToAdd);
}
} catch (IllegalAccessException ex) {
assert false;
}
}
}
}
clazz = clazz.getSuperclass();
}
return result;
}
}
MAT内存分析示例
有你前面的基础,我们就可以使用MAT(Memory Analyze Tool)分析内存泄露了。
MAT下载
先创建一个简单对象:
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
再创建一个复杂一点的对象:
public class Employ {
private User user;
private String employeeId;
private int year;
private String position;
public Employ(User user, String employeeId, int year, String position) {
this.user = user;
this.employeeId = employeeId;
this.year = year;
this.position = position;
}
}
构造一个内存溢出的场景:
import java.util.LinkedList;
import java.util.List;
/**
* -Xms10M -Xmx10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=F:/tmp/mat
*/
public class MemoryLeakMain {
public static void main(String[] args) {
List<Employ> employs = new LinkedList<>();
User user = new User("tim", 18);
Employ employ = new Employ(user,"001", 18,"dev");
System.out.println("user size:" + MemoryPreMain.sizeOf(user) + " user full size:" + MemoryPreMain.fullSizeOf(user));
System.out.println("employ size:" + MemoryPreMain.sizeOf(employ) + " employ full size:" + MemoryPreMain.fullSizeOf(employ));
while (true){
user = new User("tim", 18);
employ = new Employ(user,"001", 18,"dev");
employs.add(employ);
}
}
}
参数说明:
- -Xms10M:初始内存
- -Xmx10M:最大内存
- -XX:+HeapDumpOnOutOfMemoryError:内存溢出时dump内存快照
- -XX:HeapDumpPath=F:/tmp/mat 指定dump快照路径,如果F:/tmp/mat目录存在即在改目录下创建java_pidxxxx.hprof文件,目录不存在mat就是文件名
输出:
user size:24 user full size:24
employ size:32 employ full size:56
我们看到:
- user的Shallow Heap和Retained Heap一样
- employ的Shallow Heap比Retained Heap小
我们看一下employ,Shallow Heap的大小32计算好说,就是每个引用4字节,原生类型按字节加再加本身对象头,加padding。
对象头12字节 + 3个引用 * 4字节 + 1个int * 4字节 = 28字节 + 4字节padding = 32字节
Retained Heap的56字节怎么算的呢?
很简单: Employ对象的Shallow Heap 32字节 + User对象的Shallow Heap 24字节 = 56字节
实际的逻辑比较复杂,可以参考MemoryPreMain的fullSizeOf方法。
探测内存泄露代码位置:
查看,可能内存造成内存泄露对象信息:
具体对象内存: