对象的内存布局
在 HotSpot 虚拟机中,对象在内存中的布局主要分为三部分:对象头(Header)
、实例数据(Instance Data)
、对齐填充(Padding)
。
如图所示,对象头主要包含两部分数据: MarkWord、类型指针。其中第一部分数据 MarkWord 用于存储
哈希码(HashCode)
、GC分代年龄
、锁状态标志位
、线程持有的锁、偏向线程ID等信息。这部分数据长度在32位和64位虚拟机中的长度为32bit和64bit。对象头的另外一部分是类型指针,即对象指向他的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。另外,如果对象是一个 Java 数组,那么对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息来确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小 。
接下来的实例数据(Instance Data)是对象真正存储的有效信息,也是代码中所定义的各种类型的字段信息。
第三部分对其填充并不是一定存在的,也没有特别的意义,只是起到了占位的作用。存在的原因是因为 HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,换句话说就是对象的大小必须是8字节的整数倍,又因为对象头部分正好是8字节的整数倍,所以当实例数据部分没有对齐时,就需要通过对齐填充来补全。
二:对象头的 MarkWord
为了能够更加具体形象的看到对象的内存布局,我们使用 OpenJDK 的 JOL 包来做实验,先添加 maven 依赖。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
先定义一个普通的 Java 对象,代码如下:
public class Person {
String str = "test";
Son son = new Son();
}
class Son{
}
新定义的Person有两个属性,一个是普通的字符串,还有一个是自定义的类对象。接下来我们利用 JOL 包下的 ClassLayout 来输出他的内存布局,代码如下:
public class SeeBin {
public static void main(String[] args) throws InterruptedException {
Person person = new Person();
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
}
代码中初始化了一个 Person 对象,随后便直接打印了他的内存布局,输出结果如下:
图中的1、2、3、4分别对应 MarkWord、类型指针、实例数据、对齐填充。
- MarkWord:共8字节,该对象刚新建,还处于无锁状态,所以锁标识位是 01
- 类型指针:共4字节,标识新建的 person 属于哪个类
- 实例数据:共8字节,定义的 Person 有两个属性,str 和 son,他们对应的类型分别为 String 和 Son,这两个属性每个占4个字节
- 对齐填充:共4个字节,前三个部分所占大小相加 8+ 4+ 8 = 20,不是8的整数倍,所以得填充4个字节凑齐24字节。描述信息中也有说明: loss due to the next object aligment
三:MarkWord 的锁信息
前面我们提到对象头 MarkWord 中主要保存有锁信息、GC信息、HashCode。那么我们现在就看下 MarkWord 中的锁信息。锁主要有偏向锁、轻量级锁、重量级锁,锁之间的升级过程我们就不在这里说明了,可以参考《深入理解 Java 虚拟机》,各种锁在 MarkWord 中锁标识位的使用情况如下图所示:
对照上图,再结合之前图二中对新建对象的内存模型输出结果,结果中表示锁信息的二进制是 001,再对照图三,是一个无锁状态。
接下来再试一种情况,我们知道 HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁,为此,我们先让程序 sleep 5s后再去输出对象的内存模型,修改后的代码如下:
public class SeeBin {
public static void main(String[] args) throws InterruptedException {
//线程sleep 5s,确保虚拟机中偏向锁开启
Thread.sleep(5000);
new Thread(new Runnable() {
@Override
public void run() {
Person person = new Person();
synchronized (person){
//当前锁对象第一次被线程获取
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
}
}).start();
}
}
这段代码的输出结果如下图:
在上一段代码中,我们让线程 sleep 5s并新建一个线程去获取锁对象,因为当前只有一个线程去获取这个锁对象,所以虚拟机会把对象头中的标志位设置为偏向锁(101),并将获取这个锁的线程 ID 放到对象的 MarkWord 中,具体的字节对应可以将图四与图三中的偏向锁部分结合来看。
下面是一些能够模拟重量级锁的代码,就不细说了。
/**
* 重量级锁
*/
public class SeeBin {
static Person person = new Person();
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
//三个线程去竞争访问一个锁对象
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (person){
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
}
}).start();
}
}
}
我的知乎主页链接:知乎链接