Java对象布局
一个 Java 对象在内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)。可以用下图来清晰表示:
1、对象头
Mark Word: 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
hash(25) + age(4) + lock(3) = 32bit #32位系统 unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统
Klass Word:类型指针,指向该对象在方法区中的 Class 类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小)
如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(多加四字节的内存:4byte/32bit)
2、实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来
3、对齐填充
对齐填充没有特别的意义,仅仅起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全
Java数据类型
基本数据类型(primitive type)
引用类型 (reference type)
除了对象本身之外,还存在一个指向它的引用(指针),指针占用的内存在64位虚拟机上8个字节,如果开启指针压缩是4个字节,默认是开启的。
字段重排序
为了更高效的使用内存,实例数据字段将会重排序。排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference
class FieldTest{
byte a;
int c;
boolean d;
long e;
Object f;
}
//重排序后结果
OFFSET SIZE TYPE DESCRIPTION
16 8 long FieldTest.e
24 4 int FieldTest.c
28 1 byte FieldTest.a
29 1 boolean FieldTest.d
30 2 (alignment/padding gap)
32 8 java.lang.Object FieldTest.f
简单实例
一个int值在Java中为4byte,其包装对象Integer大小为:
4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte
所以可以看出,包装类型比基本数据类型占用的空间大得多!!
类似,一个Integer数组大小为:
4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte
对象的实际大小
浅堆(Shallow Heap)
对象本身占用的内存,不包括内部引用对象的大小,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐。
保留集(Retained Set)
对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合。
对象 A 可以类比于可达性分析中的GC root元素,如果对象 A 不存在了,则此时不可达的节点都属于对象A的保留集
深堆(Retained Heap)
指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小
下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,A 的实际大小为 A、C、D 三者之和,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内
复杂实例
1、
public class C {
private int i;
private char[] cc;
public C() {
i = 5;
cc = new char[]{'a', 'b', 'c'};
}
}
Shallow Size
: 12(C Header) + 4 (i instance) + 4 (cc reference) + 4(padding) = 24bytes
Retained Size
: 12(C Header) + 4 (i instance) + 4 (cc reference) + (16(cc header,注意是数组对象头) + 2(instance) * 3+ 2(padding)) + 4(padding) = 48bytes
2、
class Fruit {
private int size;
}
public class Apple extends Fruit {
private int size;
private String name;
private Apple brother;
private long create_time;
}
Apple大小:12(Apple Header) + 4(Fruit.size instance) + 4(Apple.size instance) + 4(name reference) + 4(brother reference) + 8(create_time instance) + 4(padding) = 40bytes
如果假设已经初始化string = “apple”,则单独计算name内部会占用 :
String类型:12(header) + 4(int) + 4 (int)+ 4(char[] reference) = 24
char[]类型:12 (header)+ 4(int,计算char[]长度的) + (2*5)(“Apple”)+ 6(padding)=32
因此,此时深堆大小为40+24+32 = 96bytes
参考:
3、(27条消息) 一个Java对象和Hashmap对象占用多大内存_Apple_Boy的博客-CSDN博客_hashmap占用内存大小