对象在 Hotspot 虚拟机中,除了对象本身的实例数据(Instance Data)之外,其在内存中的布局还包括了对象头(Object Header)和对齐填充(Padding)。
对齐填充的字节保留主要是出于 JVM 对 Java 对象内存占用的要求来考虑的。每个对象在 JVM 内存占用的大小应为 1 个字长(8 bits)的倍数,因此每个对象最后会有几个字节用于补全,没有特别的功能。
因此本帖不再赘述对齐填充,就专门来聊聊对象头。
Hotspot 虚拟机的对象头包括两(三)部分信息:
- 对象自身的运行时数据
- 类型指针
- 数组长度(前提是该对象是数组)
运行时数据(Mark Word)
包括哈希码(hash code)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
Mark Word 的数据长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别是 32 bits 和 64 bits。
对象需要存储的运行时数据很多,其实已超出了 32、64 位 Bitmap 结构所能记录的限度;不过,对象头信息耗费的是与对象自身定义数据无关的额外存储成本。
基于这一点,再考虑到虚拟机的空间效率:Mark Word 被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,且会根据对象的状态复用自己的存储空间。
比如:在 32 位 Hotspot 虚拟机中,如果对象未被锁定,那么 Mark Word 的 32 bits 的空间将被如下分配:
- 25 bits 被用于存储对象的 hash code
- 4 bits 用于存储对象分代年龄
- 1 bit 固定为 0(因其未被锁定,不是偏向锁状态)
- 2 bits 用于存储锁标志位
如下表的第一行所示。
而在其他状态下,对象的运行时数据存储内容如下:
场景(状态) | 存储内容占用的空间 | ||||
---|---|---|---|---|---|
锁状态 | 25 bits | 4 bits | 1 bit | 2 bits | |
23 bits | 2 bits | 是否是偏向锁(biased_lock) | 锁标志位 | ||
无锁 | 对象的 hash code | age 分代年龄 | 0 | 01 | |
偏向锁 | 偏向锁记录的线程 ID | epoch | age 分代年龄 | 1 | 01 |
轻量级锁 | pointer_to_lock_record 指向栈中锁记录的指针 | 00 | |||
重量级锁 | pointer_to_heavyweight_monitor 指向互斥量(重量级锁)的指针 | 10 | |||
GC 标记 | 空 | 11 |
在 64 位环境中,对象的运行时数据存储分布如下:
场景(状态) | 存储内容占用的空间 | |||||
---|---|---|---|---|---|---|
锁状态 | 56 bits | 1 bit | 4 bits | 1 bit | 2 bits | |
是否是偏向锁(biased_lock) | 锁标志位 | |||||
无锁 | 未使用空间(25 bits) | 对象的 hash code(31 bits) | cms_free | age 分代年龄 | 0 | 01 |
偏向锁 | 偏向锁记录的线程 ID(54 bits) | epoch(2 bits) | cms_free | age 分代年龄 | 1 | 01 |
轻量级锁 | pointer_to_lock_record 指向栈中锁记录的指针 | 00 | ||||
重量级锁 | pointer_to_heavyweight_monitor 指向互斥量(重量级锁)的指针 | 10 | ||||
GC 标记 | 空 | 11 |
一个对象在一个时间点总是处于其中的一个状态,只是状态之间可能会切换。
参数说明:
biased_lock
:对象第一次被线程索取锁的时候,线程 ID 会写入 Mark Word 内(32 位系统的前 23 bits,或 64 位系统的前 54 bits)
- 并且将 Mark Word 的“偏向锁”那一位设置为 1
- 下次该线程想要获取锁的时候,直接检查对象头中保存的 ID 是否等于自身线程 ID,如一致则认为当前线程获取了锁
- 这样就不需要再次获取锁了,略过了轻量级和重量级两种锁的加锁阶段,提高效率。
epoch
:验证偏向锁有效性的时间戳。
cms_free
:说起来就跟内存分配策略有关系了。
- CMS 是基于清理算法的收集器(Concurrent Mark-Sweep),相应的内存分配策略就是空闲列表 free list;
- 内存碎片问题即是将不可达对象维护到一个列表 free list 里面,这些对象占用的空间为可用空间,随时分配给线程来使用;
- 因此,可以大概推断该占位是用来标记对象是否在列表中。
age
:一共 4 位,最大值也就是 15:这就决定了为什么晋升到老年代的年龄设置(-XX:MaxTenuringThreshold
,也是已完成的 GC 次数)不能超过 15 的原因。
pointer_to_heavyweight_monitor
:指向互斥量(重量级锁)的指针。
- 在 Java 中,每个对象都持有一个属于自己的 Monitor 对象(在 Hotspot 虚拟机中由 C++ 类
ObjectMonitor
实现),指向它的指针就是 pointer_to_heavyweight_monitor - 这也正好说明了:在 Java 中,任何一个对象都可以作为锁的存在
- 参数的名字带了 monitor,也是锁被称为“监视器锁”的原因。
接下来聊聊 Mark Word 里面很重要的锁标志位。
锁标志位
锁标志位 和 是否为偏向锁 两者共同对应到唯一的锁状态。
对象状态 | 组合 | 是否为偏向锁 | 锁标志位 |
---|---|---|---|
无锁 | 001 | 0 | 01 |
偏向锁 | 101 | 1 | 01 |
轻量级锁(CAS + 失败尝试) | -00 | - | 00 |
重量级锁(互斥) | -10 | - | 10 |
GC 标记 | -11 | - | 11 |
所以说 synchronized
锁的是对象,而且分 4 种程度不同的锁。
锁只能升级不能降级,但是偏向锁可以被重置为无锁状态(也就是改一个位的值的事)。
类型指针(Klass pointer)
你没看错,就是这个 “klass”。
它用来存储对象的类型指针,该指针指向它所属类的元数据。JVM 通过这个指针来确定这个对象是哪一个类的实例。
举个例子:1
2
3
4Person p = new Person();
// p 是实例,保存在栈中,是对新的 Person() 对象的引用
// Person() 对象在堆中,保存了一个自己的类型指针 klass pointer
// Person() 对象要在元空间 metaspace 找到元数据 instanceKlass,靠的就是 klass pointer
该指针的位长度为 JVM 的一个字大小,跟 Mark Word 的大小是一样的:32 位 JVM 的长度位 32 bits,64 位的则是 64 bits。
如果应用对象过多,使用 64 位的指针会浪费大量的内存。此时为了节约内存,可以开启指针压缩 -XX:+UseCompressedOops
将长度压缩为 32 位。
数组长度
如果对象是一个数组,那么对象头还需要有额外空间用于存储数组长度。
具体长度随 JVM 架构差别而不同,跟 Mark Word 和 klass pointer 的大小也是一样的:32 位 JVM 的长度位 32 bits,64 位的则是 64 bits。
如果在 64 位 JVM 上开启了指针压缩的话,该区域长度也会被压缩为 32 位。
最后问个小问题:
32 位的 Hotspot JVM 中,Integer
对象的大小是拆箱 int
的几倍?
解:Mark Word 32 bits,4个字节;类型指针同样 4 个字节;至于实例数据,Integer
只有一个 int
类型的成员变量 value,大小 4 字节:以上合起来 12 字节,加上补齐,一共 16 字节,是 int 的 4 倍。