0%

Java对象

本文默认基于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {

public static void main(String[] args) {

Class aClz = A.class;

Class arrayClz = int[].class;

Class classClz = java.lang.Class.class;

System.out.println(aClz instanceof java.lang.Class);
System.out.println(arrayClz instanceof java.lang.Class);
System.out.println(classClz instanceof java.lang.Class);
}
}

class A {

}

执行结果为:

1
2
3
true
true
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
2
3
javac -classpath ".:./jol-core-0.16.jar" Main.java

java -XX:-UseCompressedOops -XX:-CompactFields -XX:FieldsAllocationStyle=1 -classpath ".:./jol-core-0.16.jar" Main

2.2.1、实验1——主要针对对象头

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.openjdk.jol.info.ClassLayout;

public class Main {
public static void main(String[] args) {

System.out.println(ClassLayout.parseClass(Class.class).toPrintable(String.class)); //1

System.out.println(ClassLayout.parseClass(Class.class).toPrintable(Integer.class)); //2

System.out.println(ClassLayout.parseClass(String.class).toPrintable("hello world"));//3

int[] aa = new int[]{1, 2, 3};
System.out.println(ClassLayout.parseClass(int[].class).toPrintable(aa)); //4
}
}

执行实验命令后:

  • //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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.openjdk.jol.info.ClassLayout;

public class Main {
public static void main(String[] args) {

MyObject a = new MyObject();

System.out.println(ClassLayout.parseClass(MyObject.class).toPrintable(a));

}
}


class MyObject {
boolean a;

byte b;

char c;

short d;

int e;

float f;

long g;

double h;

Object i;
}

执行实验命令后结果如图5。

图5

2.2.3、实验3——主要针对实例数据

本实验主要是为了说明:在非静态内部类实例中隐式含有一个对象引用类型字段,该字段值指向相应外部类实例对象。

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.openjdk.jol.info.ClassLayout;

public class Main {

public static void main(String[] args) {
Main main = new Main();
main.jol();
}

public void jol() {
InnerClass c = new InnerClass();
System.out.println(ClassLayout.parseClass(c.getClass()).toPrintable(c));
}

class InnerClass {
int a;
}
}

执行实验命令后结果如图6。

图6

2.2.4、实验4——主要针对对齐填充

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.openjdk.jol.info.ClassLayout;

public class Main {

public static void main(String[] args) {
MyObjectC c = new MyObjectC();

System.out.println(ClassLayout.parseClass(MyObjectC.class).toPrintable());
}
}


class MyObjectCParent {
char k;
byte l;
}


class MyObjectC extends MyObjectCParent {
boolean a;
byte b;
char c;
short d;
int e;
float f;
long g;
double h;

Object o;
}

执行实验命令后结果如图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

您的支持将鼓励我继续分享!