从理论到实践,刨根问底探索Java对象内存布局
所谓对象的内存布局,就是对象在分配到内存中后的存储格式。
对象在内存中的布局一共包含三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
第一部分:对象头
首先来看一下对象头的结构
Java对象头分为两部分:
- Mark Word:对象自身运行时数据。
- Klass Pointer:类型指针,即对象指向它的类元数据的指针。
1、Mark Word
为啥叫Mark Word呢?我理解因为这部分是用来标记对象运行时的数据和状态,比如对象的HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。而Word呢,是因为这段信息是用一个Word(字)长度来保存的。在32位系统中,一个字是32bit,也就是4字节。64位系统中,一个字是64bit,也就是8字节。
对于这部分的描述,从markOop.hpp源码的注释中即可得知。
下面就以32位的虚拟机为例,来探寻一下对象头的Mark Word部分是什么样的数据结构。
先划重点,锁状态很重要
这里要注意两点:
- 对象头的数据格式和对象的锁状态紧密相关。在不同的锁状态下,对象头的结构都不一样。其目的是为了尽量在极小的空间内存储尽量多的信息。
- 锁状态的标志位是固定的,无论是32位还是64位的虚拟机,对象头中最后两位就是锁的状态标志。
既然锁的状态很重要,那么就先看一下下锁标志对应的状态含义:
lock | 状态 |
---|---|
01 | 无锁 |
00 | 轻量级锁(locked) |
10 | 重量级锁(monitor,inflated lock) |
11 | GC标记(marked) |
01-无锁状态下的Mark Word结构
无锁状态下,涉及到两种情况:
- 稀松平常的无锁状态
- 偏向锁
是否存在偏向锁,我们也是用1位长的标识来判断。
当不存在偏向锁时,1-25位是对象的HashCode,之后的4位是对象GC的分代年龄,之后的1位是偏向锁的标志,此时该标志为0。
当存在偏向锁时,1-23为是持有偏向锁的线程的ID,之后的2位是偏向时间戳,然后4位依旧是对象GC的分代年龄,再之后的1位是偏向锁的标志,此时该标志为1。
00-轻量级锁状态下的Mark Word结构
轻量级锁状态时,对象头的前30位保存指向持有锁的线程的栈帧中锁记录的指针。
此时,获取了该对象偏向锁的线程,会在线程的栈帧上建立锁记录的空间,并通过CAS的方式将对象头的信息复制到锁记录的位置,并将对象头替换成指向锁记录的指针。
10-重量级锁状态下的Mark Word结构
当有两个及以上的线程竞争同一个锁,则轻量级锁就会升级成重量级锁。此时对象头的前30位保存的是指向重量级锁Monitor的指针。
关于Monitor,这里可以做个简单的理解:Java的重量级锁,是通过一个Monitor对象来实现的。JVM通过Monitor对象中的_owner、_EntryList来维护是哪个线程持有这个对象的锁,以及后续的阻塞线程。源码在objectWaiter.hpp中可以深入了解。
11-GC标记
当最后两位为11时,代表被GC标记了,则对象头前面的30位信息为空。
Mark Word 小结
在这里根据上面的描述,画个图来展示一下Mark Word这部分数据在32位系统和62位系统里的布局,更直观清晰一些。
2、Klass Pointer(类型指针)
紧跟着Mark Word,是对象头的另一部分——类型指针。类型指针也是用一个字的长度(32位系统是4byte,64位系统是8byte)来保存的。这个指针会指向该对象对应的类元数据,说人话就是,JVM通过这个指针知道这个对象是哪个类的实例。
3、数组长度
如果这是一个普通的Java对象,则对象头中只有Mark Word和Klass Pointer两部分。当它是一个数组对象时,对象头中还需要一部分空间来保存数组的长度。有了数组长度,JVM才能够知道一个数组对象的大小。数组长度这部分也是用一个字的长度(32位系统是4byte,64位系统是8byte)来保存。
如果64位的JVM开启了+UseCompressedOops选项,则类型指针和数组长度这两个区域都会被压缩成32位。
第二部分:实例数据
这部分就是对象真正存储的有效信息,也就是类里定义的各种类型字段的内容。
第三部分:对齐填充
这部分没有实际的含义,仅仅是起到占位符的作用。因为JVM要求对象起始地址必须是8字节的整数倍,也就是一个对象的大小必须是8字节的整数倍,所以如果一个对象的实例数据不满足8字节的整数倍,则需要做一个对齐填充的操作,保证对象的大小是8字节的整数倍。
实践一下
接下来我们来跑几个demo看看真实的对象头布局,借助JOL(Java Object Layout),我们可以分析JVM中对象的布局。
环境:64位系统,JDK8。
1、引入JOL依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2、一个简单的类
public class OneObject {
private int id;
private String name;
private double score;
}
3、打印对象内存布局
这里选择了五种情况:
- 刚刚new出来的新鲜对象
- 经历过gc后的对象
- 加锁时的对象(偏向锁)
- 多线程竞争锁时的对象(重量级锁)
- 算一下对象的hashCode
代码如下:
@Test
public void showObjectData() throws InterruptedException {
OneObject object = new OneObject();
log.info(\"初始化后的对象布局:{}\", ClassLayout.parseInstance(object).toPrintable());
System.gc();
log.info(\"gc一次之后的对象布局:{}\", ClassLayout.parseInstance(object).toPrintable());
synchronized (object) {
log.info(\"加锁时的对象布局:{}\", ClassLayout.parseInstance(object).toPrintable());
}
for (int i = 0; i < 2; i++) {
Thread thread = new Thread(()->{
synchronized (object) {
log.info(\"竞争锁时的对象布局:{}\", ClassLayout.parseInstance(object).toPrintable());
}
});
thread.start();
}
Thread.sleep(500);
object.hashCode();
log.info(\"计算完hashCode的对象布局:{}\", ClassLayout.parseInstance(object).toPrintable());
}
4、输出结果
执行以上的代码,输出结果如下:
00:06:03.877 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 初始化后的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
12 4 int OneObject.id 0
16 8 double OneObject.score 0.0
24 4 java.lang.String OneObject.name null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00:06:03.890 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - gc一次之后的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
12 4 int OneObject.id 0
16 8 double OneObject.score 0.0
24 4 java.lang.String OneObject.name null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00:06:03.891 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 加锁时的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 90 99 0b 6d (10010000 10011001 00001011 01101101) (1829476752)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
12 4 int OneObject.id 0
16 8 double OneObject.score 0.0
24 4 java.lang.String OneObject.name null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00:06:03.892 [Thread-1] INFO com.esparks.pandora.learning.vm.LearnObjectData - 竞争锁时的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 62 d9 00 20 (01100010 11011001 00000000 00100000) (536926562)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
12 4 int OneObject.id 0
16 8 double OneObject.score 0.0
24 4 java.lang.String OneObject.name null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00:06:03.893 [Thread-2] INFO com.esparks.pandora.learning.vm.LearnObjectData - 竞争锁时的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 62 d9 00 20 (01100010 11011001 00000000 00100000) (536926562)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
12 4 int OneObject.id 0
16 8 double OneObject.score 0.0
24 4 java.lang.String OneObject.name null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00:06:04.398 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 计算完hashCode的对象布局:com.esparks.pandora.learning.vm.OneObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 63 c1 aa (00001001 01100011 11000001 10101010) (-1430166775)
4 4 (object header) 56 00 00 00 (01010110 00000000 00000000 00000000) (86)
8 4 (object header) 38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
12 4 int OneObject.id 0
16 8 double OneObject.score 0.0
24 4 java.lang.String OneObject.name null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
对象布局分析
这里我们先以第一种情况的对象布局来简单说明输出的格式,直接在图上标明啦。
这里要插一句,JOL在输出对象头的时候,是按照四个字节的长度从内存中获取对象的对象头数据。所以你会看到64位的Mark Word会拆成两行(两个4字节)打印出来。
几个问题
看到这里的时候,脑海里不禁冒出几个问题。
Q1:为什么Klass pointer只有四个字节呢?
因为JVM默认开启了+UseCompressedOops选项,所以Klass Pointer被压缩成了32位。如果在启动时配置了-UseCompressedOops选项,那么Klass Pointer就也是64位啦。
Q2:说好了Mark Word的最后两位是锁状态,这刚创建的对象,最后两位怎么就是00了呢?
这个就要和字节存储的大小端模式有关了。
举个例子,一个16进制的整数0x12345678,对应的二进制整数为:00010010 00110100 01010110 01111000(12 34 56 78),一共占用四个字节。那么在内存中应该如何存储这长度为4byte的字节序列的数据呢?
有两种方式:
- 按照内存地址的顺序,依次保存12 34 56 78这四个字节的数据。这种将字节序列的高序字节存储在内存的起始地址上的方式,叫大端模式。
- 按照内存地址的顺序,依次保存78 56 34 12这四个字节的数据。这种将字节序列的低序字节存储在内存的起始地址上的方式,叫小端模式。
而一般我们用的x86或者ARM的CPU,采用的都是小端模式来保存内存中的字节序列,所以和我们常见的顺序是反着的。因此,你看到的输出的前两行的对象头,实际上的值是这样的:
所以对象初始化后,就是无锁状态啦。
分析不同状态时的对象头
刚才已经就刚初始化的对象分析过一次内存布局了。而在锁状态不同的情况下,变化也只限于对象头中Mark Word的值变动,所以这里就快速的分析一下其余的四种状态时的Mark Word了。这里我也自动将输出的小端格式转换成正常的顺序来分析。也是通过实际情况来回顾验证一下刚才的理论知识啦,对照前面的图片中不同的颜色比对就可以了。
1.gc一次之后的Mark Word
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001
红色的01代表无锁状态,蓝色的0代表无偏向锁,黄色的0001是GC分代年龄。因为GC过一次,该对象没有被回收,年龄加1。
2.加锁时的Mark Word
00000000 00000000 00000000 00000001 01101101 00001011 10011001 10010000
红色的00代表轻量级锁状态,绿色的一串是指向持有锁的线程的栈帧中锁记录的指针。
3.竞争锁时的Mark Word
00000000 00000000 00000000 00000001 00100000 00000000 11011001 01100010
红色的10代表重量级锁状态,绿色的一串是指向重量级锁Monitor的指针。
4.计算完hashCode的Mark Word
00000000 00000000 00000000 01010110 10101010 11000001 01100011 00001001
红色的01代表此时回归到无锁状态,,蓝色的0代表无偏向锁,黄色的0001是GC分代年龄为1,紫色这一串是刚才计算的hashCode,保存在了这里。所以只有在调用了hashCode()
方法时,JVM才会把对象的hashCode保存到对象头中。
总结
好啦,总结一下。
本篇文章先是介绍了Java对象的内存布局(由对象头、实例数据、对齐填充三部分组成);之后详细的介绍了对象头的数据结构(Mark Word、Klass Pointer、数组长度),以及不同锁状态下(01无锁、00轻量级锁、10重量级锁、11GC标记),Mark Word中的数据格式以及代表的含义;最后通过JOL打印出对象的内存布局,进一步验证了前半部分枯燥的理论知识。
希望看到这里,能让你彻底的理解Java对象在内存中的完整样貌啦~
参考资料
- markOop.hpp源码,主要在注释中
- objectWaiter.hpp源码
- 《深入理解Java虚拟机》
来源:https://www.cnblogs.com/codeflyer/p/16159530.html
本站部分图文来源于网络,如有侵权请联系删除。