JVM 构造对象及对象的内存布局

Java 程序中,有多种新建对象的方式,除了常见的 new 语句之外,还可以通过反射机制、Object.clone方法、反序列化以及 Unsafe.allocateInstance 方法来新建对象。其中,Object.clone 方法和反序列化通过直接赋值已有的数据,来初始化创建对象的实例字段。Unsafe.allocateInstance 方法则没有初始化字段,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。

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

// fu f = new fu(); 编译而成的字节码
new f
dup
invokespecial f()
astore_1

Java 对构造器有很多约束。首先,如果一个类没有定义任何构造器, Java 编译器会自动添加一个无参构造。

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

然后,子类的构造器需要调用父类的构造器。如果父类存在无参构造,该调用是隐式的,也就是说 Java 编译器会自动添加对父类构造器的调用。若父类没有无参构造,子类构造器必须显示的调用父类带参构造。

显示调用分两种,一种是直接使用“super”关键字调用父类构造器,而是使用“this”调用同一个类中其他含有可以调用父类的构造。无论哪种,都需要作为构造器的第一条语句,以便优先初始化继承而来的父类字段。

当调用一个构造器时,它将优先调用父类的构造器,直至 Object 类。这些构造器的调用者皆为同一对象,也就是通过 new 指令新建而来的对象。通过 new 指令新建出来的对象,它的内存其实涵盖了所有父类中的实例字段。也就是说,虽然子类无法访问父类的私有实例字段,或者子类的实例字段隐藏了父类的同名实例字段,但是子类的实例还是会为这些父类实例字段分配内存的。

这些字段在内存中的具体分布是怎么样的呢?

压缩指针

在 Java 虚拟机中,每个 Java 对象都有一个对象头(object header),这个由标记字段和类型指针所构成。其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC信息以及锁信息,而类型指针则指向该对象的类。

在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位,即,每一个 Java 对象在内存中的额外开销就是 16 个字节。以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%。这也是为什么 Java 要引入基本类型的原因之一。

为了尽量较少的内存使用量,64位Java 虚拟机引入压缩指针的概念,将堆中原本 64 位 Java 对象指针压缩成 32 位。这样对象头中方的类型指针也会被压缩成 32 位,使得对象头的大小从 16 字节降至 12 字节。当然,压缩指针不仅可以作用于对象头的类型指针,话可以作用于引用类型的字段,以及引用类型数组。

打个比方,路上停着的全是房车,而且每辆房车恰好占据两个停车位。现在,我们按照顺序给它们编号。也就是说,停在 0 号和 1 号停车位上的叫 0 号车,停在 2 号和 3 号停车位上的叫 1 号车,依次类推。

原本的内存寻址用的是车位号。比如说我有一个值为 6 的指针,代表第 6 个车位,那么沿着这个指针可以找到 3 号车。现在我们规定指针里存的值是车号,比如 3 指代 3 号车。当需要查找 3 号车时,我便可以将该指针的值乘以 2,再沿着 6 号车位找到 3 号车。

这样一来,32 位压缩指针最多可以标记 2 的 32 次方辆车,对应着 2 的 33 次方个车位。当然,房车也有大小之分。大房车占据的车位可能是三个甚至是更多。不过这并不会影响我们的寻址算法:我们只需跳过部分车号,便可以保持原本车号 *2 的寻址系统。

但以上模型有一个前提:每辆车都从偶数号车位停起。这个概念叫做 “内存对齐”。

默认情况下, Java 虚拟机堆中对象的起始地址需要对齐至8的倍数。如果一个对象用不到 8N 个字节,暧昧空白的那部分空间会浪费,浪费的空间称为对象间的填充。

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

在对压缩指针解引用时,我们需要将其左移 3 位,再加上一个固定偏移量,便可以得到能够殉职 32 GB地址空间的伪 64 位指针了。此外,可以通过内存对齐来进一步提升寻址范围。但是,这同时也可能增加对象间填充,导致压缩指针没有达到原本节省空间的效果。

好比规定每辆车都需要从偶数车位号停起,那么对于占据两个车位的小房车来说刚刚好,而对于需要三个车位的大房车来说,也仅是浪费一个车位。但是如果规定需要从 4 的倍数号车位停起,那么小房车则会浪费两个车位,而大房车至多可能浪费三个车位。

当然,就算是关闭了压缩指针,Java 虚拟机还是会进行内存对齐。此外,内存对齐不仅存在于对象与对象之间,也存在于对象中的字段之间。比如说,Java 虚拟机要求 long 字段、double 字段,以及非压缩指针状态下的引用字段地址为 8 的倍数。

字段内存对齐的其中一个原因,是让字段只出现在同一 CPU 的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。

字段重排列

字段重排列,就是 Java 虚拟机重新分配字段的先后顺序,以达到内存对齐的目的。Java 虚拟机中又三种排列方法,都会遵守两个规则:

  • 其一,如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。以 long 类为例,它仅有一个 long 类型的实例字段。在使用了压缩指针的 64 位虚拟机中,尽管对象头的大小为 12 个字节,该 long 类型字段的偏移量也只能是 16,而中间空着的 4 个字节便会被浪费掉。
  • 其二,子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。在具体实现中,Java 虚拟机还会对齐子类字段的起始位置。对于使用了压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N;而对于关闭了压缩指针的 64 位虚拟机,子类第一个字段则需要对齐至 8N。

eg:

class A {
  long l;
  int i;
}
 
class B extends A {
  long l;
  int i;
}

B 类在启用压缩指针和未启用压缩指针时,各个字段的偏移量。

# 启用压缩指针时,B 类的字段分布
B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION
      0     4        (object header)
      4     4        (object header)
      8     4        (object header)
     12     4    int A.i                                       0
     16     8   long A.l                                       0
     24     8   long B.l                                       0
     32     4    int B.i                                       0
     36     4        (loss due to the next object alignment)

当启用压缩指针时,可以看到 Java 虚拟机将 A 类的 int 字段放置于 long 字段之前,以填充因为 long 字段对齐造成的 4 字节缺口。由于对象整体大小需要对齐至 8N,因此对象的最后会有 4 字节的空白填充。

# 关闭压缩指针时,B 类的字段分布
B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION
          4        (object header)
          4        (object header)
          4        (object header)
          4        (object header)
          8   long A.l
          4    int A.i
          4        (alignment/padding gap)                  
          8   long B.l
          4    int B.i
          4        (loss due to the next object alignment)

当关闭压缩指针时,B 类字段的起始位置需对齐至 8N。这么一来,B 类字段的前后各有 4 字节的空白。那么可以将 B 类的 int 字段移至前面的空白中,从而节省这 8 字节。

猜你喜欢

转载自blog.csdn.net/qq_40488936/article/details/106361876