HotSpot 对象探秘之对象的内存布局
概述
Java 虚拟机规范不强制规定对象的内部结构应当如何表示,在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以分为如下三部分:
- 对象头( Header )
- 实例数据( Instance Data )
- 对齐填充( Padding )
本文将为大家讲解对象在堆内存中是怎么存储的。
一、对象头( Header )
HotSpot 虚拟机对象的对象头部分包括两类信息:
第一类:存储对象自身的运行时数据
第一类用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁,偏向线程 ID、偏向时间戳等。
这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 比特和 64 比特,官方称它为 “Mark Word” (标记字) 。
对象需要存储的运行时数据很多,其实已经超出了 32、64 位 Bitmap 结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word 被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态服用自己的存储空间。
例如:在 32 位的 HotSpot 虚拟机中,如对象未被同步锁锁定的状态下,Mark Word 的 32 个比特存储空间中的 25 个比特用于存储对象哈希码,4 个比特用于存储对象分代年龄,2 个比特用于存储锁标志位,1 个比特固定为 0,
在其他状态(轻量级锁定、重量级锁定、GC 标记、可偏向)下对象的存储内容如下表:
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC 标记 |
偏向线程 ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
第二类:用于存储类型指针
类型指针就是对象指向它的类型元数据的指针, Java 虚拟机通过这个指针来确定该对象是哪个类的实例。
但是,并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,就是查找对象的元数据信息并不一定要经过对象本身,这点将会在 《HotSpot 对象探秘之对象的访问定位》 这篇文章进行讲述。
除此之外,如果对象是一个 Java 数组,那么在它的对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息来确定 Java 对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
二、实例数据( Instance Data )
实例数据部分是对象真正存储的有效信息,即我们再程序代码里所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录下来。
这部分的存储顺序会受到虚拟机分配策略参数( -XX:FieldsAllocationStyle )和字段在 Java 源码中定义的顺序所影响。
HotSpot 虚拟机默认的分配顺序为 longs/doubles 、ints、shorts/chars、bytes/booleans、oops( Ordinary Object Pointers, OOPs ), 从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个条件的前提下,在父类中定义的变量会出现在子类之前。
如果 HotSpot 虚拟机的 +XX:CompactFields 参数值为 true(默认就为 true),那子类之中较窄的变量也允许插入到父类变量的空隙之中,以节省一些空间。
三、对齐填充( Padding )
对齐填充部分并不是必须存在的,也没有特别的意义,只是起着占位符的作用。
在 HotSpot 虚拟机的自动内存管理系统要求,对象起始地址必须为 8 字节的整数倍,换句话说就是任何对象的大小都必须是 8 字节的整数倍。对象头部分已经被精心设计成 8 字节的倍数,因此如果对象的实例数据没有对齐的话,就需要通过对齐填充来补全。
引用文献
[1] 深入理解Java虚拟机 第三版 --周志明