实探java对象的内存布局
在我深入学习synchronized的时候,我查了很多资料,发现synchronized锁住的是对象的对象头,然后我又了解什么是对象头,这块的资料就很多了。
总结一下就是:
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充数据。
对象头:对象头主要结构是由Mark Word 和 Class Metadata Address
实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
之后再就是详细的介绍对象头占多大,每个字节代表啥意思等等。
说了一堆,我知道了synchronized的锁的原理,知道了对象头的作用,知道了对象在内存中的布局。总之就是理论的东西知道了不少。但是我还是想实际看看对象在内存中的布局到底是啥样的,然后我又找啊找,终于让我找到了。
首先引入这个依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
第一次
我新建了一个空对象O
public class O {
}
然后将对象内存布局结构打印出来
public static void main(String[] args) {
O o = new O();
//获取对象内存布局
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
下面是打印出来的结果
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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
发现了对象头是12个字节,总大小是16字节,空对象没有属性,方法,所以实例数据是0字节,有4字节的数据填充,验证了虚拟机要求对象起始地址必须是8字节的整数倍。
对象头 | 12 byte |
---|---|
实例数据 | 0 byte |
填充数据 | 4 byte |
对象大小 | 16 byte |
第二次
新建一个对象F。
public class F {
int f = 0;
}
对象O继承对象F,里面包含静态属性,非静态属性,数组,引用对象,方法。
public class O extends F{
int i; // 非静态属性
int[] arr = new int[5]; //数组
Object object = new Object(); //相当于属性
static int b = 5; //静态属性
private void a(){
int a = 0;
}
}
打印出来的结果
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) 81 c1 00 f8 (10000001 11000001 00000000 11111000) (-134168191)
12 4 int F.f 0 //父类属性
16 4 int O.i 0 //非静态属性
20 4 int[] O.arr [0, 0, 0, 0, 0] //非静态属性数组(4bit,包含了数组的长度)
24 4 java.lang.String O.s (object) //引用类型属性(4bit)
28 4 java.lang.Object O.object (object) //引用类型属性(4bit)
Instance size: 32 bytes //总大小32byte(正好是8的整数倍,没有填充数据)
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
验证了 实例数据是存放类的非静态属性数据信息(引用类型都是4字节),包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。不保存静态属性和方法信息。
对象头 | 12 byte |
---|---|
实例数据 | 20 byte |
填充数据 | 0 byte |
对象大小 | 32 byte |
从这两次就可以看出来
- 对象头总是占用12个字节(这里也有的人说占用了16个字节,是因为Class Metadata Address默认开启了指针压缩,没有开启就是16个字节,涉及到硬件的设计了,这里就不说了,感兴趣的朋友自己搜索看看。)
- 实例数据没有就不占用字节
- 总字节是8的倍数,不够数据填充补足
- 实例数据存放的非静态成员变量
这样我们的知识就不是只停留在理论阶段了,关于对象的内存布局自己可以亲自动手看看到底是啥样的!
实例数据和对象填充其实这里就没有什么好说的,通过上面的演示大家应该知道了是什么意思,那么这里最重要的就是对象头了。
对象头
对象头:对象头主要结构是由Mark Word 和 Class Metadata Address
这是我从其他的文章里面看到的,但是我现在想去验证一下,所以我去查看了一下 OpenjdK关于hotSpot的文档
这个相当于JVM规范中定义的对象头的概念,翻译一下
object header(对象头)
每个GC管理的堆对象开头的通用结构。 (每个oop都指向一个对象头。)包括有关堆对象的布局,类型,GC状态,同步状态和标识哈希码的基本信息。 由两个词组成。 在数组中,紧随其后的是长度字段。 请注意,Java对象和VM内部对象都具有通用的对象标头格式。
mark word
每个对象头的第一个部分。 通常,一组位域包括同步状态和标识哈希码。 也可以是指向与同步相关的信息的指针(具有特征性的低位编码)。 在GC期间,可能包含GC状态位。
klass pointer
每个对象头的第二个部分。 指向另一个对象(元对象),该对象描述原始对象的布局和行为。 对于Java对象,“容器”包含C ++样式“ vtable”。
放在谷歌翻译里面翻译的,原谅我英文比较差,就不润滑翻译了。
到这里我就稍微了解了下,对象头分为两部分,第一部分是mark word,存放的同步状态,哈希码,与同步相关的指针,GC状态。第二部分是klass pointer至于为什么其他文章写的是Class Metadata Address我也就不深究了,因为我也不知道,但是意思上还是都差不多的,类的指针和类的元数据地址,都是代表的是指向这个对象所在类的地址。
上面的东西说白了还是概念,只不过是官方的权威的概念,接下来我还是想看看对象头的具体实现。包括哪些数据存放哪些东西。所以我下载了OpenJDK的源码来找找代码。
至于怎么下载openjdk的源码,我从openjdk的官方下载禁止下载,然后到github上搜一下openjdk就好了。
在markOop.hpp(对应的就是mark word)文件中,分为32位JVM和64位JVM
从上图可以知道32bit的 占用32bit,
hash | 25bit |
---|---|
age(GC分代年龄) | 4bit |
biased_lock(偏向锁) | 1bit |
lock | 2bit |
64bit的占用64bit,具体的还是看上图。
看到这里想到以前看过的一个面试题,每发生一次young GC,存活的对象分代年龄增加1,到多少的时候会进入老年代,答案是15,为什么是15,不能是16吗?如果以前肯定是懵逼的,我哪儿知道为什么是15,今天应该就知道了,从上面知道对象头中保存的分代年龄,不管32位还是64位的虚拟机都是4bit, 最大值是1111,即16,保存的范围是0~15,所以分代年龄是15。当然如果后面虚拟机变更成age 用5位表示,那对象的分代年龄最大就可以达到31次。
我们这里使用的是64的JVM,所以对象头的mark word是占用了64bit即8个字节,上面图中对象头总共是12个字节,那么剩下的4个字节就是保存klass pointer即指向类的信息的指针。
mark word :64bit
klass pointer : 32bit
这里解释一下,指针是需要用64位来表示的,但是由于64位太耗内存,所以采用了指针压缩的方法,来用32位表示64位的地址。其他的涉及到的东西太底层,不是咱们讨论的东西,感兴趣的朋友可以网上随便搜索下指针压缩,大量的文章让你借鉴参考。
mark word
接下来就是重点看看mark word中保存的东西,在不同状态下都是如何表示的。
对象在不同状态下mark word表示的含义是不同的。
对象总共有5种状态:
无状态(刚 new 出来),偏向锁,轻量级锁,重量级锁,gc(对象要被回收)
下图是对象不同状态下mark word表示的含义
这里对象的状态有5种,但是lock是占用2位,总共能表示4种状态 即00,01,10,11。所以这里(biased_lock)偏向锁联合lock来表示5种状态。
我们这里来看下无锁状态的mark word
从上面的图中我们知道无锁状态下的mark word
unsed:25 | hashcode:31| unsed:1 | age:4 | biased_lock:1 | lock:2
下面我们实际证明一下
public static void main(String[] args) {
O o = new O();
//计算对象的hashcode值,16进制表示
System.out.println(Integer.toHexString(o.hashCode()));
//获取对象内存布局
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
实际输出的mark word
理论的mark word
unsed:25 | hashcode:31| unsed:1 | age:4 | biased_lock:1 | lock:2
实际的mark word
(00000001 00101110 01001100 01011111)
(01101000 00000000 00000000 00000000)
根据理论说unsed:25 ,意思前25位应该都是0,但是和实际的不一样,我陷入了懵逼,后来查阅了资料发现,我用的笔记本是微软的架构,存储是小端存储,说白了就是倒着存(不是每一位倒着存,是每一个字节倒着存),至于为啥我也不知道,太深奥了,我不想知道为啥,我只想验证下。所以我们就倒着看,发现后25位确实是0,
unsed:25 | hashcode:31 | unsed:1 | age:4 | biased_lock:1 | lock:2 |
---|---|---|---|---|---|
黄色 | 绿色 | 红色 | 蓝色 | 灰色 | 粉红色 |
我上面的实际输出图中输出了hashcode,和这里比对的值是一样的,说明mark word的存储确实是这样子的。
文章就先讲到这里了,关于对象其他状态下的验证,大家可以自行去试试。