概览
一个对象根据不同情况可以被划分成两种情况,当对象是一个非数组对象的时候,对象头,实例数据,对齐填充在内存中三分天下,而数组对象中在对象头中多了一个用于描述数组对象长度的部分
对象头
对象头分为两部分,第一部分称之为 “Mark Word”,第二部分是用于获取该对象类型的 类型指针,如果是数组对象还包括记录数组长度的数据。
在不同的操作系统中,这些区域所占的内存也不同,在 32 位的系统中,MarkWord 占用 32bit 的空间(也就是 4 字节)。类型指针和数组长度数据一样合作占用 32bit 的空间。
在 64 位的操作系统中,MarkWord 占用 64bit 的空间,类型指针在不开启指针压缩(CompressedOOPs)的情况下是 64bit(8 byte),而在开启指针压缩的情况下,仅剩 32bit(4 byte)
Mark Word
这一部分存储的是 对象自身的运行时数据,这一块儿内容的数据结构并不固定,它会根据对象的状态复用自己的存储空间,
这是摘自 markOop.hpp 文件中的片段,其中表示了对象的以下五种状态:
标志位 | 偏向锁标识位 | 状态 |
---|---|---|
01 | 0 | 无锁 |
01 | 1 | 偏向锁 |
00 | 无 | 轻量级锁 |
10 | 无 | 重量级锁 |
11 | 无 | GC Mark |
我们接下来接着去看 MarkWord 的结构:
在这里我们可以看到,初始化的时候只是定义了无锁和偏向锁状态的结构(上半部分是没有开启 COOPs- 指针压缩的结构,下半部分是开启了指针压缩的结构),
当处于轻量级锁、重量级锁时,记录的对象指针,根据 JVM 的说明,此时认为指针仍然是 64 位,最低两位假定为 0; 当处于偏向锁时,记录的为获得偏向锁的线程指针,该指针也是 64 位;
更多的内容我们就不再这里扩展了,根据反馈的情况,我会在后面并发编程中单开一篇来聊聊锁的进化之路。
类型指针
这个东西有时候会用到去确定该对象属于 哪个类的实例,也有用不到的时候,这个要根据不同的虚拟机对于对象的定位实现算法的选择来进行(比如 HotSpot JVM 就使用该类型指针去获取该对象类型数据)
实例数据
实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的 字段内容,这里的字段内容不仅仅包括当前类的字段,也包括他的父类中所定义的字段。
这部分的存储规则遵循虚拟机分配策略参数和字段在 Java 源码中的定义顺序,HotSpot JVM 默认的分配策略是 long/double,int,short/char,byte/boolean,oops(普通对象指针,Ordinary Object Pointers)也可以理解为 reference,关于指针压缩我们下节去说。
这里需要注意,在父类中定义的变量会出现在子类前,但是我们可以通过将 CompactFileds 参数设置为 true,将子类中较小的变量插入到父类大变量的空隙中。
对齐填充
这部分内容并不是必须存在的,因为 Hot Spot JVM 中规定了 对象的大小必须是 8 字节的整数倍 ,在 C /C++ 中类似的功能被称之为 内存对齐,内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
内存对齐遵循两个规则:
- 假设第一个成员的起始地址为 0,每个成员的起始地址(startpos)必须是其数据类型所占空间大小的整数倍
- 结构体的最终大小必须是其成员(基础数据类型成员)里最大成员所占大小的整数倍。
这里也就不难理解为什么 JVM 规定对象的大小必须是 8 字节的整数倍了,因为在 64 位系统下(不开启指针压缩),对象中存在很多占用 8 byte 的数据类型。但是同时也存在一些 4 byte 的数据类型,这时我们的 Padding 就起到了作用,去补充不满 8 byte 的部分,凑齐 8 的整数倍。
公众号