以下所有数据均为64位环境下。
java对象大小由以下几部分组成:对象头 + 实例数据 + 对齐填充 。
对象头
对象头 = 标记部分(mark word) + 原始对象引用
标记部分记录了该对应的运行时数据,如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分大小在32位机器上为4byte,64位机器上为8byte。
原始对象引用就是对象的指针,通过这个指针找到对象的实例,该数据可以压缩。这部分大小在32位机器上为4byte,在64位机器上为8byte,如果开启了压缩(UseCompressedOops),大小为4byte。jdk8默认开启压缩。
由上述概念可知,一个对象头的大小 = 标记部分(8byte) + 原始对象引用(未压缩:8byte/压缩:4byte)=未压缩:16byte/压缩:12byte。
实例数据
原始数据大小
类型 | 大小(单位:byte) |
byte | 1 |
boolean | 1 |
char | 2 |
short | 2 |
int | 4 |
float | 4 |
double | 8 |
long | 8 |
对象引用大小为32位 4byte,64位8byte(开启压缩后为4byte)。
对齐填充
对齐填充说的是任何对象都用8byte来对齐,所以对象的大小为8的整数倍。
所以一个没有任何参数的控对象,对象大小为16byte。
浅堆
浅堆大小为对象本身的大小,浅堆大小 = 对象头 + 实例数据 + 对齐填充
深堆
深堆大小是对象所涉及的所有对象的大小之和。
保留内存
保留内存是指只能通过该对象访问的对象浅堆之和。
举例:假设5个对象:A,B,C,D,E。A引用B和C,D引用C和E
A的深堆大小 = A + B + C
A的保留内存大小 = A + B,因为D引用了C。
保留内存为对象回收后,肯定释放的内存,上述例子中,回收A,肯定会回收B,但是不会回收C,因为D引用了C。
实例代码:
package com.test
public class Test {
A a = new A();
B b = new B();
public static void main(String[] args) throws Exception {
Test t = new Test();
System.in.read();
}
}
class A {
private String a = "a";
private byte[] b = new byte[1024 * 2];
}
class B {
private double d = 2D;
}
使用 jmap -dump获得程序的内存快照,在mat(MyEclipse Memory Analyzer tools)中打开,找到com.test.Test类,查看浅堆,深堆(Retained Heap虽然字面上是保留内存,但是实际是指的深堆)大小。
开启压缩,图如下:
![Image 1][]
可以看到com.test.Test的浅堆大小为24byte,深堆大小为2184byte。
根据代码分析com.test.Test对象浅堆大小 = 对象头(12byte)+ A引用(4byte) + B引用(4byte)+ 对齐填充(4byte) = 24byte
com.test.A对象浅堆大小 = 对象头(12byte) + 字符串a引用(4byte)+ 数组引用(4byte)+ 对齐填充(4byte)=24byte
com.test.B对象浅堆大小 = 对象头(12byte)+ 基本类型double变量(8byte)+ 对齐填充(4byte)= 24byte
byte数组b浅堆大小 = 对象头(12byte)+ 2048 * 基本类型byte的大小 = 12 + 2048 = 2064byte
String类中包含两个参数int类型的hash 和 char数组,所以String类型的a也占24byte。
[Image 1]: