Java 对象的布局

发布时间 2023-04-25 11:28:16作者: 变体精灵

一、概述

在 Hotspot 虚拟机中,对象的内存布局主要由 3 部分组成
1、对象头(Header): 包括对象的运行时状态信息 Mark Word、Klass Pointer(类型指针,直接指针访问方式)、Array Length(如果是数组对象,才会有此特殊内存区域)
2、实例数据(Instance Data): 普通对象的实例数据包括当前类声明的实例字段以及父类声明的实例字段,而 Class 对象的实例数据包括当前类声明的静态字段和方法表等信息
3、对齐填充(Padding): Hotspot 虚拟机对象的大小必须按 8 字节对齐,如果对象实际占用空间不足 8 字节的倍数,则会在对象末尾增加对齐填充,8 字节的整数倍是计算机信息存储的规范,方便数据的存储和读取

 

二、各区域介绍

1、对象头

  • Mark Word

Mark Word 是对象运行时的状态信息,包括哈希码、分代年龄、锁状态、偏向锁信息等.由于 Mark Word 是与对象实例数据无关的额外存储成本,因此虚拟机选择将其设计为带状态的数据结构,会根据对象当前的不同状态而定义不同的含义

对象头中 Mark Word 大小是和机器字长保持一致的,对于 32 位的计算机 Mark Word 长度为 32 bit(4 个字节),64 位计算机 Mark Word 长度为 64 bit(8 个字节)

下面就以 64 位虚拟机为例介绍一下 Mark Word 的具体结构

64 位 Hotspot 虚拟机 Mark Word 
state 25 31 1 4 1(偏向锁位) 2(锁标志位)
Normal(无锁) unused identity_hashcode unused age 0 01
Biased(偏向锁) thread: 54                     epoch: 2 unused age 1 01
Light Weight Locked(轻量级锁) ptr_to_lock_record: 62 00
Heavy Weight Locked(重量级锁) ptr_to_heavy_monitor: 62 10
Marked for GC(GC 标志)     11
  • Klass Pointer

指向对象类型数据的指针,只有虚拟机采用直接指针的对象访问定位方式才需要在对象上记录类型指针,而采用句柄的对象访问定位方式则不需要此指针,Hotspot 采用的就是第一种访问定位方式,所以需要该指针指向具体的类元信息(Class 对象)

从 JDK1.6 update14 开始,在 64bit 操作系统中,JVM 支持指针压缩,关于指针压缩主要有两种

作用 开启(JDK 8 都默认开启) 关闭
普通对象的指针压缩 -XX:+UseCompressedOops -XX:-UseCompressedOops
当前对象的对象头中 Klass Pointer 的指针压缩 -XX:+UseCompressedClassPointers -XX:-UseCompressedClassPointers

上面两种指针压缩策略组合起来总共有 4 中新的策略

一、-XX:+UseCompressedOops -XX:+UseCompressedClassPointers

二、-XX:+UseCompressedOops -XX:-UseCompressedClassPointers

三、-XX:-UseCompressedOops -XX:-UseCompressedClassPointers

四、-XX:-UseCompressedOops -XX:+UseCompressedClassPointers

但是使用第四种策略的时候会出现警告

出现警告的具体原因如下

// UseCompressedOops must be on for UseCompressedClassPointers to be on.
if (!UseCompressedOops) {
	if (UseCompressedClassPointers) {
		warning("UseCompressedClassPointers requires UseCompressedOops");
	}
	FLAG_SET_DEFAULT(UseCompressedClassPointers, false);
}

从上面的代码可以看出想要开启 UseCompressedClassPointers 的前提是必须要先开启 UseCompressedOops,否则就会抛出上面的警告信息

UseCompressedClassPointers 参数依赖了 UseCompressedOops 参数

开启 UseCompressedOops 时,UseCompressedClassPointers 会默认自动开启

关闭 UseCompressedOops 时,UseCompressedClassPointers 也会默认自动关闭

例如开启了 -XX:+UseCompressedOops 参数之后也会默认开启 -XX:+UseCompressedClassPointers 参数,那么整个 Java 对象不止会压缩自身对象头中的 Klass Pointer 指针,同时还会压缩实例数据中的 Klass Pointer 指针

  • Array Length

普通实例对象的长度可以从类元(Class 对象)信息中推断出来,但是数组类型的实例对象长度是不能提前确定的,只有在创建时才能确定,数组对象创建之后其长度又是固定不变的,所以需要在对象的对象头中专门开辟一块内存空间来记录数组的长度,这块内存区域便是对象头中的 Array Length

 

2、Instance Data

普通对象和 Class 对象的实例数据区域是不同的
1、普通对象: 包括当前类声明的实例字段以及父类声明的实例字段,不包括类的静态字段
2、Class 对象: 包括当前类声明的静态字段和方法表等

 

3、对齐填充

需要对齐填充的目的是方便数据的存储和读取,如果当前数据长度不足 8 byte 的整数倍,为了方便下个数据存储,需要将数据长度补齐为 8 Byte 的整数倍

 

三、验证

下面所有的验证过程都是基于 JDK 8 进行的,不同版本之间可能会存在差异

maven 工程引入依赖

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.9</version>
</dependency>

1、普通对象

public class ObjectLayout {
    // 定义普通对象 obj
    private static Object obj = new Object();

    public static void main(String[] args) {
        // 当前虚拟机信息
        System.out.println(VM.current().details());
        // obj 对象的内存布局
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

1.1、开启所有对象的 Klass Pointer 指针压缩

不添加任何 JVM 参数等价于 -XX:+UseCompressedOops -XX:+UseCompressedClassPointers

同时开启了压缩当前对象的对象头中 Klass Pointer 指针和实例数据中的 Klass Pointer 指针

从上面信息可以得知,本地虚拟机是 64 位的,Mark Word 与机器字长一致,占用内存大小为 8 个字节,开启了指针压缩,所以 Klass Point 占用 4 个字节,否则占用 8 个字节

由于整个对象占用的内存大小为 8 + 4 = 12 个字节,不是 8 字节的整数倍,所以虚拟机为我们自动填充了 4 个字节

1.2、关闭当前对象的对象头中的 Klass Pointer 指针压缩

添加 JVM 启动参数 -XX:-UseCompressedClassPointers 等价于 -XX:+UseCompressedOops -XX:-UseCompressedClassPointers

关闭了压缩当前对象的对象头中 Klass Pointer 指针,开启了压缩实例数据中的 Klass Pointer 指针

从上面结果可以看出,当关闭了压缩当前对象的对象头中 Klass Pointer 指针之后,当前对象对象头中的 Klass Pointer 占用 8 个字节,由于整个对象占用的内存大小为 8 + 8 = 16 个字节,是 8 字节的整数倍,所以不需要再对齐填充了

1.3、关闭所有对象的 Klass Pointer 指针压缩

添加 JVM 启动参数 -XX:-UseCompressedOops 实际上等价于 -XX:-UseCompressedOops -XX:-UseCompressedClassPointers

所有的对象的 Klass Pointer 指针均不压缩

Mark Word 占用 8 个字节,Klass Pointer 占用 8 个字节,整个对象占用的内存大小为 8 + 8 = 16 个字节,是 8 字节的整数倍,不需要对齐填充

 

2、数组对象

public class ObjectLayout {
    public static void main(String[] args) {
        Integer[] intArr = new Integer[100];
        // Integer 类型的数组对象的内存布局
        System.out.println(ClassLayout.parseInstance(intArr).toPrintable());
    }
}

2.1、开启所有对象的 Klass Pointer 指针压缩

不添加任何 JVM 参数等价于 -XX:+UseCompressedOops -XX:+UseCompressedClassPointers

同时开启了压缩当前对象的对象头中 Klass Pointer 指针和实例数据中的 Klass Pointer 指针

Mark Word 等于机器字长占用 8 个字节,开启了 Klass Pointer 指针压缩,所以 Klass Pointer 占用 4 个字节,不压缩则占用 8 个字节

Array Length: 数组的最大长度是 2^31 -1,需要使用 4 个字节才能表示

整个对象头长度是 8 + 4 + 4 = 16,是 8 字节的整数倍,不需要对齐填充

Instance Data: 由于创建了一个 Integer 类型的数组,其长度为 100,对于实例数据而言,由于开启了压缩实例数据中的 Klass Pointer 指针,压缩后一个 Integer 元素的 Klass Pointer 占用 4 个字节,100 个元素的指针就是占用 400 个字节

Instance Data 长度是 400 字节,也是 8 的整数倍,实例数据也不需要对齐填充

2.2、关闭当前对象的对象头中的 Klass Pointer 指针压缩

添加 JVM 启动参数 -XX:-UseCompressedClassPointers 等价于 -XX:+UseCompressedOops -XX:-UseCompressedClassPointers

关闭了压缩当前对象的对象头中 Klass Pointer 指针,开启了压缩实例数据中的 Klass Pointer 指针

Mark Word 8 个字节,关闭了压缩当前对象的对象头中 Klass Pointer 指针,此时 Klass Pointer 占用 8 个字节

Array Length: 数组的最大长度是 2^31 -1,需要使用 4 个字节才能表示

整个对象头长度是 8 + 8 + 4 = 20,并不是 8 字节的整数倍,所以需要填充 4 个字节

实例数据: 开启了压缩实例数据中的对象指针,4 * 100 = 400 个字节

Instance Data: 可能会有疑问,上面的 JVM 启动参数,我们关闭了 Klass Pointer 的指针压缩(-XX:-UseCompressedClassPointers),为什么实例数据中 100 个 Integer 类型的 Klass Pointer 还是 400 个字节呢,而不是 800 个字节

这里我是这么理解的开启 -XX:-UseCompressedClassPointers 针对的当前对象的对象头中的 Klass Pointer,而不是实例数据中的 Klass Pointer

对于实例数据而言,我们开启了 -XX:+UseCompressedOops 之后,实例数据中的 Klass Pointer 依旧会压缩

2.3、关闭所有对象的 Klass Pointer 指针压缩

添加 JVM 启动参数 -XX:-UseCompressedOops 实际上等价于 -XX:-UseCompressedOops -XX:-UseCompressedClassPointers

所有的对象的 Klass Pointer 指针均不压缩

Mark Word 8 个字节,关闭了压缩对象头中 Klass Pointer 指针,此时 Klass Pointer 占用 8 个字节

Array Length: 数组的最大长度是 2^31 -1,需要使用 4 个字节才能表示,开启或者不开启指针压缩,数组的长度永远固定为 4 个字节

整个对象头长度是 8 + 8 + 4 = 20,并不是 8 字节的整数倍,所以需要填充 4 个字节

实例数据: 关闭了普通对象指针压缩,整个实例数据占用内存大小为 8 * 100 = 800 个字节

Instance Data 长度是 800 字节,也是 8 的整数倍,实例数据也不需要对齐填充

 

3、带成员变量的普通对象

class Animal{
    private int id;
    private String name;
    private boolean sweet;
}

public class ObjectLayout {
    public static void main(String[] args) {
        Animal animal = new Animal();
        // Integer 类型的数组对象的内存布局
        System.out.println(ClassLayout.parseInstance(animal).toPrintable());
    }
}

3.1、开启所有对象的 Klass Pointer 指针压缩

不添加任何 JVM 参数等价于 -XX:+UseCompressedOops -XX:+UseCompressedClassPointers

同时开启了压缩当前对象的对象头中 Klass Pointer 指针和实例数据中的 Klass Pointer 指针

3.2、关闭当前对象的对象头中的 Klass Pointer 指针压缩

添加 JVM 启动参数 -XX:-UseCompressedClassPointers 等价于 -XX:+UseCompressedOops -XX:-UseCompressedClassPointers

关闭了压缩当前对象的对象头中 Klass Pointer 指针,开启了压缩实例数据中的 Klass Pointer 指针

3.3、关闭所有对象的 Klass Pointer 指针压缩

添加 JVM 启动参数 -XX:-UseCompressedOops 实际上等价于 -XX:-UseCompressedOops -XX:-UseCompressedClassPointers

所有的对象的 Klass Pointer 指针均不压缩