本文默认基于64位的JDK 8(其虚拟机实现是HotSpot),除非特别说明。
一、Java对象
Java对象处于Java堆中,Java对象可分为:
- 普通实例对象,比如“java.lang.String类实例对象”,“java.util.List类实例对象”,“me.dslztx.A自定义类实例对象”
- 数组对象,比如“java.lang.String[]数组对象”,“int[]数组对象”
- Class对象,较为特殊,具有两种身份:1)作为一切Java类(当然也包括“java.lang.Class”类)的Class对象,然后这些Java类的静态字段数据放置在其相应的Class对象中[1];2)作为“java.lang.Class”类的实例对象。参见以下源代码1和相应的执行结果可帮助理解
源代码1:
1 | public class Main { |
执行结果为:
1 | true |
二、Java对象的内存布局
2.1、基本说明
在HotSpot虚拟机中,Java对象的内存布局可分为3块区域:对象头,实例数据和对齐填充。
需要说明的是,Java对象的内存布局除了跟具体的虚拟机实现(这里是基于64位的JDK 8所对应的具体HotSpot虚拟机实现)有关之外,还跟很多因素有关,包括但不限于以下:
- JVM参数
-XX:+/-UseCompressedOops
,说明见下面 - JVM参数
-XX:+/-CompactFields
,说明见下面 - JVM参数
-XX:FieldsAllocationStyle=N
,说明见下面 @sun.misc.Contended
注解,说明见[2]- …
2.1.1、对象头
包括3个部分,描述见表1。
表1
名称 | 描述 | 占据字节数 |
---|---|---|
Mark Word | 存储对象自身的运行时数据,比如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等 | 8个字节 |
类元数据指针(Klass Pointer) | 指向相应Klass对象(即方法区内的“类元数据”,详见《方法区》)的指针。注意不是指向Java堆中相应Class对象的指针,这点极易混淆 | 跟JVM参数-XX:+/-UseCompressedOops 有关,其表示是否开启指针压缩。当关闭(-XX:-UseCompressedOops )时,指针大小为8字节;当开启-XX:+UseCompressedOops 时,指针大小为4字节 |
数组长度记录区域 | 当对象类型为数组时,本区域记录数组长度;否则,本区域为空 | 当对象类型为数组,占据4个字节;否则,占据0个字节 |
2.1.2、实例数据
存储一系列字段数据,字段类型可以是8种原生基本类型(boolean,byte,char,short,int,float,long,double)之一或者是对象引用类型(reference),不同类型字段数据占据字节数描述见表2。
表2
字段类型 | 占据字节数 |
---|---|
boolean | 1个字节 |
byte | 1个字节 |
char | 2个字节 |
short | 2个字节 |
int | 4个字节 |
float | 4个字节 |
long | 8个字节 |
double | 8个字节 |
reference | 跟JVM参数-XX:+/-UseCompressedOops 有关,其表示是否开启指针压缩。当关闭(-XX:-UseCompressedOops )时,占据字节数为8字节;当开启-XX:+UseCompressedOops 时,占据字节数为4字节 |
特别需要注意的是,本文的“字段数据”都是指“非静态字段数据”,因为“静态字段数据”都放置在相应的Class对象中,这与“一、Java对象”小节中对“Class对象”的描述是呼应的。
2.1.3、对齐填充
对齐填充规则可归纳为如下几条:
规则1:8种原生基本类型或者对象引用类型字段内存起始地址需满足“相应类型占据字节数-对齐”,比如“int”类型字段的内存起始地址需满足“4-对齐”,“reference”类型字段的内存起始地址需满足“8-对齐”
规则2:同一类中所有字段按照以下字段类型顺序排列分配内存,字段类型顺序为“long/double,int/float,char/short,boolean/byte,reference”
规则3:父类中所有字段排列于子类中所有字段之前
规则4:假定父类中存在至少一个字段,那么子类中首字段内存起始地址需满足“8-对齐”
规则5:如果是“数组对象”,那么具体元素队列中第一个元素的内存起始地址需满足“8-对齐”
规则6:如有必要需进行字节填充,以使得接下来毗邻的Java对象内存起始地址满足“8-对齐”
备注:
- JVM参数
-XX:+/-CompactFields
含义是:是否开启将对象中较窄的数据插入到间隙中。如果开启,上述规则2不再适用[3] - JVM参数
-XX:FieldsAllocationStyle=N
含义是:设定字段分配策略。如果-XX:FieldsAllocationStyle!=1
,上述规则2不再适用[3]
对齐填充一般遵循上述对齐填充规则,但还受到“JVM参数-XX:+/-CompactFields
”,“JVM参数-XX:FieldsAllocationStyle=N
”,“@sun.misc.Contended
注解”等因素影响,使得最终策略相当复杂,笔者的目标只是概要理解,故没有必要追求理解准确和完备的对齐填充策略。
2.2、实验
接下来进行实验,实验环境为:
- 基于64位的JDK 8(其虚拟机实现是HotSpot)
-XX:-UseCompressedOops
-XX:-CompactFields
-XX:FieldsAllocationStyle=1
- 其他因素暂不考虑
OpenJDK项目下提供了一个名为“jol”的工具,它的名字全称为“Java Object Layout”,由名称即可知,该工具能够提供关于Java对象的内存布局信息。详细的关于“jol”的介绍和使用说明见链接。笔者在本文中使用jol-core-0.16.jar
版本。
故最终实验命令如下:
1 | javac -classpath ".:./jol-core-0.16.jar" Main.java |
2.2.1、实验1——主要针对对象头
实验代码:
1 | import org.openjdk.jol.info.ClassLayout; |
执行实验命令后:
- //1语句对应的打印结果见图1
- //2语句对应的打印结果见图2
- //3语句对应的打印结果见图3
- //4语句对应的打印结果见图4
图1
图2
图3
图4
分析图1和图2,发现两处的Klass Pointer值(即“object header: class”描述对应的值,不过这里有个瑕疵就是不该用“class”文案,而该用“klass”文案,否则会造成歧义)都为0x00007f29f2740058
,契合“String类的Class对象和Integer类的Class对象都是java.lang.Class的实例对象,故Klass Pointer都指向方法区中相应于java.lang.Class的同一个类元数据——Klass对象”。
2.2.2、实验2——主要针对实例数据
实验代码:
1 | import org.openjdk.jol.info.ClassLayout; |
执行实验命令后结果如图5。
图5
2.2.3、实验3——主要针对实例数据
本实验主要是为了说明:在非静态内部类实例中隐式含有一个对象引用类型字段,该字段值指向相应外部类实例对象。
实验代码:
1 | import org.openjdk.jol.info.ClassLayout; |
执行实验命令后结果如图6。
图6
2.2.4、实验4——主要针对对齐填充
实验代码:
1 | import org.openjdk.jol.info.ClassLayout; |
执行实验命令后结果如图7。
图7
参考文献
[1]http://openjdk.java.net/jeps/122
[2]《伪共享》
[3]https://blog.csdn.net/qq_34212276/article/details/117914322
[4]http://psy-lob-saw.blogspot.com/2013/05/know-thy-java-object-memory-layout.html
[5]https://www.zhihu.com/question/59174759
[6]https://www.zhihu.com/question/38496907/answer/156793201