《深入拆解Java虚拟机》学习笔记 Day10 -- Java对象的内存分布

89 阅读4分钟

Java如何新建对象

  • New
  • 反射机制
  • Object.clone 方法
  • 反序列化
  • Unsafe.allocateInstance 方法.

其中,Object.clone 方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。

Unsafe.allocateInstance 方法则没有初始化实例字段;

而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。

以 new 语句为例,它编译而成的字节码将包含用来请求内存的 new 指令,以及用来调用构造器的 invokespecial 指令。


// Foo foo = new Foo(); 编译而成的字节码
  0 new Foo
  3 dup
  4 invokespecial Foo()
  7 astore_1

如果一个类没有定义任何构造器的话, Java 编译器会自动添加一个无参数的构造器。


// Foo类构造器会调用其父类Object的构造器
public Foo();
  0 aload_0 [this]
  1 invokespecial java.lang.Object() [8]
  4 return

总而言之,当我们调用一个构造器时,它将优先调用父类的构造器,直至 Object 类。这些构造器的调用者皆为同一对象,也就是通过 new 指令新建而来的对象。

通过 new 指令新建出来的对象,它的内存其实涵盖了所有父类中的实例字段。也就是说,虽然子类无法访问父类的私有实例字段,或者子类的实例字段隐藏了父类的同名实例字段,但是子类的实例还是会为这些父类实例字段分配内存的。

Java对象的内存分布

对象头 object header

在 Java 虚拟机中,每个 Java 对象都有一个对象头(object header),这个由标记字段类型指针所构成。

在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位(bit),而类型指针又占了 64 位(bit)。即每一个 Java 对象在内存中的额外开销就是 16 个字节(128bit = 128/8 = 16字节)

以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%(16字节/4字节=4)。这也是为什么 Java 要引入基本类型的原因之一。

标记字段

其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息.

类型指针

而类型指针则指向该对象的类。

压缩指针

为了尽量较少对象的内存使用量,64 位 Java 虚拟机引入了压缩指针的概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本 64 位的 Java 对象指针压缩成 32 位的。

这样一来,对象头中的类型指针也会被压缩成 32 位,使得对象头的大小从 16 字节降至 12 字节。当然,压缩指针不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段,以及引用类型数组。

压缩指针原理

以停车为例,本来一个车占2个车位,找第一辆车是0、1车位,找第二辆车是2、3车位,这样找比较麻烦. 可以按照车的顺序来找,例如找第3辆车,位置为3*2 + 起始位置. 这个概念类似内存对其.

内存对齐(对应虚拟机选项 -XX:ObjectAlignmentInBytes,默认值为 8)

默认情况下,Java 虚拟机堆中对象的起始地址需要对齐至 8 的倍数。如果一个对象用不到 8N 个字节,那么空白的那部分空间就浪费掉了。这些浪费掉的空间我们称之为对象间的填充(padding)。

在默认情况下,Java 虚拟机中的 32 位压缩指针可以寻址到 2 的 35 次方个字节,也就是 32GB 的地址空间(超过 32GB 则会关闭压缩指针)。

字段重排列

Java 虚拟机重新分配字段的先后顺序,以达到内存对齐的目的。

Java 虚拟机中有三种排列方法(对应 Java 虚拟机选项 -XX:FieldsAllocationStyle,默认值为 1),但都会遵循如下两个规则。

其一,如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。以 long 类为例,它仅有一个 long 类型的实例字段。在使用了压缩指针的 64 位虚拟机中,尽管对象头的大小为 12 个字节,该 long 类型字段的偏移量也只能是 16,而中间空着的 4 个字节便会被浪费掉。

其二,子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。在具体实现中,Java 虚拟机还会对齐子类字段的起始位置。对于使用了压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N;而对于关闭了压缩指针的 64 位虚拟机,子类第一个字段则需要对齐至 8N。

参考文章 10|Java对象的内存分布