前言
我们都知道Java是一种面向对象语言,而Java对象在JVM中的存储也是有一定结构的。而这个关于Java对象自身的存储模型就称之为Java对象模型。
JVM通常都是使用非Java语言实现,是用来解析并运行Java程序的,它有自己的模型来表示Java语言的各种特性,包括Object。本文以最为我们熟知的HotSpot为例,一起来学习下Java对象在JVM层面的Java对象模型。
Oop-Klass model
HotSpot底层是使用C++实现的,C++本身也是一门面向对象的语言。但HotSpot并没有根据一个Java的实例对象去创建对应的C++对象,而是设计了一个oop-class model:
- OOP(Ordianry Object Pointer):普通对象指针,标识一个实例信息。
- Klass:描述对象实例的具体类型,包含元数据和方法信息。
- 避免每个对象中都含有一个vtable(虚函数表)
上图就是HotSpot JVM的
oop-klass model,那为什么HotSpot要设计一套oop-klass model呢?
答案是:对于面向对象语言都有一个很重要的特性就是多态。但在多态的实现上,C++和Java有着本质的区别。在C++中是通过虚函数表的方式实现多态的,每个包含虚函数的类都具有一个虚函数表(virtual table),在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明顺序依次排列所有的虚函数。由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉。
在Java中,在运行时会维持类型信息以及类的继承体系。每一个类会在方法区中对应一个数据结构用于存放类的信息,可以通过Class对象访问这个数据结构。其中,类型信息具有superclass属性指示了其超类,以及这个类对应的方法表(其中只包含这个类定义的方法,不包括从超类继承来的)。而每一个在堆上创建的对象,都具有一个指向方法区类型信息数据结构的指针,通过这个指针可以确定对象的类型。
一个Java对象可以分为三部分存储在内存中,分别是:
- 对象头(包括锁状态标注,线程持有的锁等标志)
- 实例数据
- 对齐填充
对象头
对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志。在运行期间,Mark Word里存储的数据会随着是否偏向锁、锁标志位的变化而变化,如下图(64位JVM Mark Word存储结构)五种状态中其中一种,即同一时刻MarkWord只能表示其中一种锁状态.
对于锁升级的过程,将在下次针对
synchronized的学习中再详细讲解。
实例数据
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。
HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops。
从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
对齐填充
由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
总的一句话来说,“数据项仅仅能存储在地址是数据项大小的整数倍的内存位置上(分别为偶地址、被4整除的地址、被8整除的地址)”比如int类型占用4个字节,地址仅仅能在0,4,8等位置上。