Java 中的对象在 JVM 中是怎么映射
写在前面
Java 中的对象在 JVM 中是怎么映射的?这个话题一直想写。但是一直没有动笔。后来发现 Java 中的锁很多问题都与这个在 JVM 中映射的对象存在着关系。还是需要搞定它。
我们平时在写 Java 代码的时候,最常见的就是创建一个对象了。这些代码最终都是会在虚拟机上运行的。而一个对象最终在 JVM 中呈现的样子到底是什么呢?还是非常值得我们探究一番。毕竟虚拟机 HotSpot 是 C++ 实现的。
探寻过程
HotSpot 设计的一套 OOP-Klass 模型用来在 JVM 内部进行表示一个对象。这里我们需要提到一个词语 OOP-Klass 二分模型。先从这个单词的含义来理解它。OOP 是 oridinary object pointer,即普通对象指针,它是用来描述对象的实例信息。而 Klass 是代表 Java 类的 C++ 对等体,用来描述 Java 类。
Oops模块可以分为两个独立的部分:OOP 框架与 Klass 框架。
1.探寻 OOP 框架
在 Java 应用程序过程中,每次创建一个 Java 对象的时候,在 JVM 内部也会相应地创建一个 OOP 对象来表示一个 Java 对象。OOPS 类的共同基类为 oopDesc。
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
在 HotSpot 中,根据 JVM 内部使用的对象业务类型,分成了多种 oopDesc 子类。每种类型的 OOP 都代表了一个在 JVM 内部使用的特定对象模型。有点接口和具体实现类的感觉了。
类 | 作用 |
---|---|
oopDesc | OOPS 抽象基类 |
instanceOopDesc | 描述 Java 类的实例 |
methodOopDesc | 描述 Java 方法 |
constMethodOopDesc | 描述 Java 方法的只读信息 |
methodDataOopDesc | 描述 Java 方法的信息 |
arrayOopDesc | 描述数组的抽象基类 |
objArrayOopDesc | 描述容纳对象( OOPS ) 元素的数组 |
typeArrayOopDesc | 描述容纳基本类型(非 OOPS )的数组 |
constantPoolOopDesc | 描述容纳类文件中常量池项的数组 |
constantPoolCacheOopDesc | 描述常量池高速缓存 |
klassOopDesc | 描述一个 Java 类 |
markOopDesc | 描述对象头 |
当我们在 Java 中使用 new 创建一个 Java 实例对象的时候,JVM 会相应的创建一个instanceOopDesc 对象来表示这个 Java 对象。当我们使用 new 创建一个 Java 数组实例的时候,JVM 就会创建一个 arrayOopDesc 对象来代表这个数组对象。
一个对象在堆内存的存储布局可以分为三个部分,对象头( Header )、实例数据(Instance Data)、和对齐填充( Padding )。而 instanceOopDesc 或者 arrayOopDesc 就是我们提到的对象头。
对象头里面包含了哪些信息呢,它包含了两部分的信息:
一部分是存储对象运行时记录信息,如哈希码( HashCode )、GC 年代分布、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。很多地方也称这部分为 MarkWord。
另一部分是元数据指针,指向描述类型的 Klass 对象的指针。Klass 对象包含了实例对象所属类型的元数据(meta data)。Java 虚拟机通过这个指针来确定该对象是哪个类的实例。在运行的时候会使用这个指针定位到位于方法区的类型信息。
如果对象是一个 Java 数组的话,那在对象头中还有一块用于记录数组长度的数据。
对象的对齐填充,并不是一定需要存在的。它也没有啥特别大的作用。仅仅是起着占位符的作用。因为 HotSpot 虚拟机要求对象的起始地址必须是 8 字节的整数倍。也即任何对象的大小必须是 8 字节的整数倍。如果不够的时候就需要这个进行补齐。
当我们在 Java 程序里面,使用 new 关键字创建对象的时候,对象的引用保存在栈上,在堆中分配对象实例。这个除了实例数据外,JVM 还会在实例数据的前面自动加上一个对象头 instanceOopDesc 。对象的元数据( instanceKlass )保存在方法区中。
顺便回顾一下方法区是保存什么数据的,方法区是用来存储被虚拟机加载的类型信息、常量、静态常量、即使编译器编译后的代码缓存等数据。
通过栈上引用可以访问到 JVM 内部表示的该对象实例(instanceOop)。当需要调用该类的方法或者访问该类的类变量的时候。就是通过 instanceOop 持有的类元数据指针定位到方法区中的 instanceKlass 对象来完成。
2.探寻 Klass 框架
Klass 数据结构:描述类型自身的布局,以及刻画出于其他类间的关系(父类、子类、兄弟类)。Klass 是一个顶层的基类。
这里主要说一下 instanceKlass, JVM 在运行的时候,为每一个已经加载的 Java 类创建一个instanceKlass 对象。用在 JVM 层表示 Java 类。
instanceKlass 内存布局如下:
//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;
klassOop _host_klass;
objArrayOop _signers;
typeArrayOop _inner_classes;
klassOop _implementors 0;
klassOop _implementors 1;
typeArrayOop _class_annotations;
objArrayOop _fields_annotations;
objArrayOop _methods_annotations;
objArrayOop _methods_parameter_annotations;
objArrayOop _methods_default_annotations;
以上是 OOP 块的内容,在 JVM 中,对象在内存中的基本存在形式就是 OOP。
写在后面
通过以上的探索,基本上有以下的共识。很重要的两个词汇instanceOopDesc、instanceKlass需要在脑海中留下一些印象。
对象头 instanceOopDesc 包含了 MarkWord 与 元数据指针。而 instanceKlass 是用来在JVM 层面表示一个 Java 类的(保存在方法区)。其中元数据指针就是指向 JVM 层表示一个 Java 对象的 instanceKlass 。Klass 对象包含了实例对象所属类型的元数据(meta data)。自然指向它的就被称为了元数据指针了。
MarkWord 里面就包含了与 Java 中锁相关的信息了。这个后面在写一篇专门论述。