前言
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”
对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示
对象头区域
Java对象的对象头由 mark word 和 class pointer 两部分组成
对象自身的运行时数据(MarkWord)
存储 hashcode、GC 分代年龄、锁类型标记、偏向锁线程 ID、CAS 锁指向线程 LockRecord 的指针等,synchronized 锁的机制与这个部分(markwork)密切相关
,用 markword 中最低的三位代表锁的状态,其中一位是偏向锁位,另外两位是普通锁位
class pointer 存储对象的类型指针,该指针指向它的类元数据
值得注意的是,如果应用的对象过多,使用 64 位的指针将浪费大量内存。64 位的 JVM 比 32 位的 JVM 多耗费 50% 的内存
现在使用的 64 位 JVM 会默认使用选项+UseCompressedOops开启指针压缩,将指针压缩至 32 位
实例数据区域
存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按 4 字节对齐
对齐填充区域
JVM 的实现 HostSpot 规定对象的起始地址必须是 8 字节的整数倍,换句话来说,现在 64 bit/位 的 OS 往外读取数据的时候一次性读取 64 bit/位 整数倍的数据,也就是 8 个 byte/字节,所以 HotSpot 为了高效读取对象,就做了“对齐”,如果一个对象实际占用的内存大小不是 8 byte/字节 的整数倍时,就“补位”到 8 byte/字节 的整数倍。所以对齐填充区域的数据不是必须存在的,仅仅是为了字节对齐,当然大小也不是固定的
对象头存储内容
以 64 位操作系统为例,对象头存储内容图例
lock:锁状态标记位。该标记的值不同,整个 mark word 表示的含义不同
biased_lock:偏向锁标记。为 1 时表示对象启用偏向锁,为 0 时表示对象没有偏向锁
age:Java GC 标记位对象年龄
identity_hashcode:对象标识 Hash 值。采用延迟加载技术。当对象使用 HashCode() 计算后,并会将结果写到该对象头中
。当对象被锁定时,该值会移动到线程 Monitor 中
thread:持有偏向锁的线程 ID 和其他信息。这个线程 ID 并不是 JVM 分配的线程 ID 号,和 Java Thread 中的 ID 是两个概念
epoch:偏向时间戳
ptr_to_lock_record:指向栈中锁记录的指针
ptr_to_heavyweight_monitor:指向线程 Monitor 的指针
打印对象头
maven依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
创建对象 Person
@Data
public class Person {
private String name;
private boolean flag;
}
使用 jol 工具打印 Person 对象的对象头
public static void main(String[] args) {
Person p = new Person();
System.out.println(ClassLayout.parseInstance(p).toPrintable());
}