加载中...

Java对象的大小计算


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

参考:

1、尚硅谷宋红康JVM全套教程(详解java虚拟机)

2、Java中对象占用内存大小计算 - 简书

3、(27条消息) 一个Java对象和Hashmap对象占用多大内存_Apple_Boy的博客-CSDN博客_hashmap占用内存大小

4、补充:浅堆深堆与内存泄露 · 语雀


文章作者: DestiNation
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 DestiNation !
  目录