Java对象内存布局和对象头
对象在堆内存中的存储布局
在Hotspot虚拟机中,对象在堆内存中的存储布局可以分为三个部分:对象头(Header)、实例数据(Instance Data) 和对齐填充(Padding)
-
对象头(Header)(在64位系统中,MarkWord占了8个字节,类型指针占了8个字节,一共是16个字节)
- 对象标记(Mark Word):默认存储对象的hashcode、分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态服用自己的存储空间,也就是说在运行期间MarkWord里面存储的数据会随着锁标志位的变化而变化
- 类型指针(Class Pointer):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象哪个类的实例
-
实例数据(Instant Data):它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
-
对齐填充(Padding):虚拟机要求对象起始地址必须是8字节的整数倍,
填充数据不是必须存在的
,仅仅是为了字节对齐,这部分内存按8字节补充对齐
HotSpot64位虚拟机 Mark Word存储结构
- biased_lock:偏向锁标识,表示是否偏向锁,由于无锁和偏向锁的标志位都是01,为了区分它们,估引入一位的偏向锁标识位;
- lock:标志位,区分锁状态。当最后两位标志位为11时,对象进入GC回收状态;
- unused:未使用的;
- hashCode:特指identity hash code ,表示未经覆盖重写的,由jvm及内存地址计算得出的对象hash值;
- age:对象分代年龄,表示对象被GC的次数。当次数达到阈值时,对象就会被转移到老年代;age标识位,只有4位,最大能表示15,这也是-XX:MaxTenuringThreshold 这个参数最大只能设置15的原因;
- threadId:偏向线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被设置为该线程的ID。之后就无需再进行获取锁的操作;
- epoch:偏向时间戳,用于验证偏向锁有效性。偏向锁在CAS锁操作过程中表示对象更偏向哪个锁;
- pointer_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针;
- pointer_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针;
- cms_free:cms收集器有关。未开启指针压缩时,cms会将对象状态标记在 klass pointer 中,开启指针压缩后空间变小,只能将cms的标示位挪到了Mark Word中。
cms在 JDK14中 已经被移除
内存布局证明
pom.xml文件中引入 jol依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
<
<!-- <version>0.9</version> -->
</dependency>
查看VM信息
public class MemeoryTest {
public static void main(String[] args) {
// VM的详细情况
System.out.println("VM的详细情况"+VM.current().details());
//所有对象分配的字节都是8的整数倍
System.out.println("所有对象分配的字节都是 "+VM.current().objectAlignment()+" 的整数倍");
}
}
第一种情况:空对象
public class MemeoryTest {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
此图是基于 <version>0.16</version>
版本依赖的输出
基于 <version>0.9</version>
版本依赖的输出
第二种情况:非空对象
问题:按照对象内存布局中所提到的,对象头中类型指针应该是占8字节,但是此处输出的类型指针为什么都是4字节?
因为JVM 默认开启压缩指针,开启后将上述类型指针压缩为4字节
,以节约空间
验证:
使用 java -XX:+PrintCommandLineFlags -version 命令,执行后查看JVM的信息
手动关闭压缩指针
public class MemeoryTest {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
GC分代年龄证明
不加 -XX:MaxTenuringThreshold=
参数时,分代年龄默认值是:15 (最大的值为15
)
本次设置 -XX:MaxTenuringThreshold=16
运行代码直接提示报错:
无法创建虚拟机,MaxTenuringThreshold参数必须是0——15之间
原因:因为分代年龄在内存布局中占4bit,2进制4位最大值位1111 ,最大只能为15