承前启后,Java对象内存布局和对象头
大家好,我是小高先生。在我之前的一篇文章《并发编程防御装-锁(基础版)》中,我简要介绍了锁的基础知识,并解释了为什么Java中的任何对象都可以作为锁。在那里,我提到了对象头中有一个指向ObjectMonitor的指针,但没有深入探讨Java对象的内存结构。本文将引导大家深入了解Java对象的内存布局以及对象头结构,帮助大家更好地理解Java中的对象和锁,并为之后学习synchronized和锁升级打下基础。
- new Object()怎么理解?
- JOL
- GC分代年龄
new Object()怎么理解?
平时我们开发的时候经常要new一个对象,很少想过这个对象存在哪里以及这个对象是有什么构成的,所以关于new Object
的理解可归于两点:
- 位置所在(where)
学过JVM的同学都应该知道JVM内存结构,我们new出来的对象就在堆区,大对象在老年代,小对象在新生代。
- 构成布局(what)
1.对齐填充
先讲一下对齐填充,这个比较简单。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小是8字节的整数倍。对象头已经被设计成8字节的倍数,通常是1倍或2倍,如果实例数据大小不是8字节的整数倍,就需要用对齐填充添加一下,使对象大小为8字节的整数倍。
2.对象头
对象头是对象的另一个重要组成部分,它包含了一些关于对象的元信息。具体来说,对象头包括Mark Word和类元信息(类型指针)。
Mark Word
平时写代码的时候可能会通过hashcode()
方法获取对象的哈希码,那哈希码保存在哪里呢?还有JVM中垃圾回收机制里说过,当一个对象在新生代经过15次Minor GC之后还存活下来就会进入老年代,那对象怎么记录GC次数呢?这些信息都记录在Mark Word中,先不需要都知道里面的标记代表什么,后续会讲到,这里只需要知道Mark Word存储了对象的一些重要信息。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
在64位系统中,Mark Word占8字节,类型指针占8字节,一共16字节。所以当我们new一个对象,还没有实例数据,那这个对象就是16字节。64为系统下,Mark Word就是64bit大小,存储结构如下:
比如现在对象是无锁状态,那Mark Word前25bit不用,之后的31bit存hashCode,偏向锁标志位为1,锁标志位为01,其他锁状态会在相关文章中讲解。
Mark Word默认存储hashCode、分代年龄和锁标志位等相关信息,这些信息都是与对象自身定义无关的数据,会根据对象的状态复用自己的存储空间,运行期间数据会随着锁状态而改变。
类元信息(类型指针)
Phone ph = new Phone()
,创建一个对象,会在JVM方法区存放对象的Klass类元信息,对象头的类型指针就是这个对象指向它的类元信息的指针,JVM通过类型指针确定这个对象是哪个类的实例,比如我创建Animal类,new Animal()实例的类型指针就会指向方法区中的Animal的Klass类元信息。由于Animal里面没有创建属性,所以Animal对象还没有实例数据,大小为16字节。
java
public class ObjectHeadDemo {
public static void main(String[] args) {
Animal a = new Animal();
}
}
class Animal{
}
我给大家百度了Klass类元信息具体是什么,因为一开始我也不懂。
Klass类元信息是JVM中表示类的元信息的数据结构
JVM中,每个Java类都对应一个Klass实例,是用C++编写的一个类,存储了Java类的所有元信息,包括属性、方法、修饰符等。Klass模型是JVM内部使用的一个概念,是Java类的内部表示。这个模型包含了类的所有信息,如类的完整名称、访问修饰符、父类、实现的接口、属性和方法等。这些信息在类加载的过程中被读取并存储到方法区。
这么看起来,Klass就想是这个类的模板。
3.实例数据
java
class Animal{
int id;
boolean flag = false;
}
如果创建了一个Animal对象,大小就是对象头(16字节)+ int(4字节)+ boolean(1字节)= 21字节,需要有对齐填充,21 + 3 = 24字节。
JOL
有关Java对象布局的理论知识已经学完了,那能不能从代码层面验证一下对象的结构呢。
JOL(Java Object Layout)是一个专门用于分析Java虚拟机(JVM)中对象内存布局的工具箱。它能够提供非常精确的关于对象如何分布在JVM内存中的信息。
1.先在POM文件中导入坐标
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
2.通过指令查看JVM详细细节
java
public class JOLDemo {
public static void main(String[] args) {
//VM的详细细节
System.out.println(VM.current().details());
//获取当前JVM实例的对象对齐方式
System.out.println(VM.current().objectAlignment());
}
}
class Customer{
int id;
boolean flag = false;
}
Object alignment指的是对象在内存中的对齐方式。8 bytes表示对象的大小必须是8的倍数,即对象的起始地址必须是8的整数倍。
我们创建一个Object类,看看这个类具体的结构是什么样的。
参数 | 作用 |
---|---|
OFFSET | 偏移量,也就是这个字段起始位置 |
SIZE | 字节大小 |
TYPE | Class中定义的类型 |
DESCRIPTION | 类型描述 |
VALUE | 是TYPE在内存中的值 |
我们这里会发现类型指针只占了4字节,之前说的是占8字节,这是为什么?这个我们稍后讨论。
另外一个细节会发现Instance size
为16字节,因为对象头不够8的倍数,要补齐,也就是new一个对象默认大小为16字节。刚刚是创建了一个Object对象,现在再创建一个自己写的类看看结果,并且类中没有实例数据。结果表明自己写的类和Object类new出来的对象内存布局是一样的。
java
public class JOLDemo {
public static void main(String[] args) {
Object o = new Object();
//System.out.println(ClassLayout.parseInstance(o).toPrintable());
Customer c = new Customer();
System.out.println(ClassLayout.parseInstance(c).toPrintable());
}
}
class Customer{
//只有对象头,没有实例数据
}
另外一种情况是类中有实例数据,看看结果,这次除了对象头以外多了实例数据,然后也是用对齐填充补到8的倍数。
java
public class JOLDemo {
public static void main(String[] args) {
Object o = new Object();
//System.out.println(ClassLayout.parseInstance(o).toPrintable());
Customer c = new Customer();
System.out.println(ClassLayout.parseInstance(c).toPrintable());
}
}
class Customer{
int id;
String name;
}
现在来研究研究为什么类型指针是4而不是8,这是因为压缩指针 的影响。使用java -XX:+PrintCommandLineFlags -version
指令,启动Java程序的时候把所有参数打印出来。-XX:+UseCompressedClassPointers
这个参数就表示使用了压缩指针,默认开启,结果就导致类型指针是4字节。
试试不开启指针压缩是什么样,正常工作中是不会用到的,这里就是跟大家尝试一下,看看结果而已。
GC分代年龄
GC年龄在Mark Word中用4bit存储,最大为15,也就是JVM参数-XX:MaxTenuringThreshold = 15
。我们也可以验证一下,调整参数改成16试试,运行时是有异常的。
总结
本文和朋友们一起学习Java对象内存布局的知识,对象由对象头、实例数据和对齐填充组成。对象头包括Mark Word和类型指针,Mark Word记录一些对象的关键信息,主要包括锁的状态,类型指针是可以确定这个对象是哪个类的实例。JVM要求对象大小是8字节的整数倍,所以当对象头和实例数据大小不是8字节整数倍的时候,就需要对齐填充帮忙补齐。
东西不多但是挺重要的,可以更好地理解之前讲的synchronized底层分析,也可以对未来要学的锁升级做好准备。