jvm-对象的内存布局

对象内的布局是:最前面是对象头,有两个VM内部字段:_mark 和 _klass。

后面紧跟着就是对象的所有实例字段,紧凑排布,规则如下:

  • 继承深度越浅的类所声明的字段越靠前,继承深度越深的类所声明的字段越靠后。
  • 在同一个类中声明的字段按字段的类型宽度来重排序,对普通Java类默认的排序是:long/double - 8字节、int/float - 4字节、short/char - 2字节、byte/boolean - 1字节,最后是引用类型字段(4或8字节)。
  • 每个字段按照其宽度来对齐;最终对象默认再做一次8字节对齐。在类继承的边界上如果有因对齐而带来的空隙的话,可以把子类的字段拉到空隙里。

这种排布方式可以让原始类型字段最大限度地紧凑排布在一起,减少字段间因为对齐而带来的空隙;同时又让引用类型字段尽可能排布在一起,减少OopMap的开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class A {
boolean b;
Object o1;
}

class B extends A {
int i;
long l;
float f;
Object o2;
}

class C extends B {
boolean b;
}

它的实例对象布局就是:(假定是64位HotSpot VM,默认开启压缩指针的话)

1
2
3
4
5
6
7
8
9
10
11
-->  +0 [ _mark     ] (64-bit header word)
+8 [ _klass ] (32-bit header word, compressed klass pointer)
+12 [ A.b ] (boolean, 1 byte)
+13 [ (padding) ] (padding for alignment, 3 bytes)
+16 [ A.o1 ] (reference, compressed pointer, 4 bytes)
+20 [ B.i ] (int, 4 bytes)
+24 [ B.l ] (long, 8 bytes)
+32 [ B.f ] (float, 4 bytes)
+36 [ B.o2 ] (reference, compressed pointer, 4 bytes)
+40 [ C.b ] (boolean, 1 byte)
+41 [ (padding) ] (padding for object alignment, 7 bytes)

所以C类的对象实例大小,在这个设定下是48字节,其中有10字节是为对齐而浪费掉的padding,12字节是对象头,剩下的26字节是用户自己代码声明的实例字段。

留意到C类里字段的排布是按照这个顺序的:对象头 - Object声明的字段(无) - A声明的字段 - B声明的字段 - C声明的字段——按继承深度从浅到深排布。而每个类里面的字段排布顺序则按前面说的规则,按宽度来重排序。同时,如果类继承边界上有空隙(例如这里A和B之间其实本来会有一个4字节的空隙,但B里正好声明了一些不宽于空隙4字节的字段,就可以把第一个不宽于4字节的字段拉到该空隙里,也就是 B.i 的位置)。

同时也请留意到A类和C类都声明了名字为b的字段。它们之间有什么关系?——没关系。
Java里,字段是不参与多态的。

派生类如果声明了跟基类同名的字段,则两个字段在最终的实例中都会存在;派生类的版本只会在名字上遮盖(shadow / hide)掉基类字段的名字,而不会与基类字段合并或令其消失。上面例子特意演示了一下A.b 与 C.b 同时存在的这个情况。