Java 对象的布局
一、概述
在 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
但是使用第四种策略的时候会出现警告
出现警告的具体原因如下
1 2 3 4 5 6 7 | // 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 工程引入依赖
1 2 3 4 5 | <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version> 0.9 </version> </dependency> |
1、普通对象
1 2 3 4 5 6 7 8 9 10 11 | 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、数组对象
1 2 3 4 5 6 7 | 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、带成员变量的普通对象
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 指针均不压缩
举例
一个字符串 "我是一只快乐的小毛毛" 占用内存大小
1 2 3 4 5 6 7 8 9 10 | public class AllocationDemo { public static void main(String[] args) { String str = new String( "我是一只快乐的小毛毛" ); // 这里包含的是 str 对象的长度,实例数据部分 char 型数组只是存放一个引用,占用 4 个字节, 8(64 位虚拟机 Mark Word) + 4(压缩后的 klassPoint) + 4(int 类型 hash) + 4(char 类型数组引用) + 4(对齐填充) = 24 个字节 System.out.println(ClassLayout.parseInstance(str).toPrintable()); // 整个对象的内存大小,"我是一只快乐的小毛毛" 这个字符串是存储在 char 型数组中的,char 数组占用内存大小 // 8(64 位虚拟机 Mark Word) + 4(压缩后的 klassPoint) + 4(数组长度) + 10 * 2(一个字符占用两个字节) + 4(对齐填充) = 40 个字节 System.out.println(GraphLayout.parseInstance(str).toFootprint()); } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?