Java 中的对象在 JVM 中是怎么映射

月伴飞鱼

共 3456字,需浏览 7分钟

 · 2021-10-20

写在前面

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 内部使用的特定对象模型。有点接口和具体实现类的感觉了。

作用
oopDescOOPS 抽象基类
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 中锁相关的信息了。这个后面在写一篇专门论述。

浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报