【JVM】底层实现(一):浅谈 OOP-Klass 对象模型

当我们在写 Java 代码的时候,我们会面对着无数个接口,类,对象和方法。但我们有木有想过,Java 中的这些对象、类和方法,在 HotSpot JVM 中的结构又是怎么样呢?HotSpot JVM 底层都是 C++ 实现的,那么 Java 的对象模型与 C++ 对象模型之间又有什么关系呢?今天就来分析一下 HotSpot JVM 中的对象模型:oop-klass model。

1.OOP-Klass 模型概述

HotSpot JVM 并没有根据 Java 实例对象直接通过虚拟机映射到新建的 C++ 对象,而是设计了一个 oop-klass 模型。oop 指的是 Ordinary Object Pointer(普通对象指针,指向被创建的对象,具体通过什么结构创建对象请向下看…),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象;而 klass 则包含元数据和方法信息,用来描述 Java 类。

那么为何要设计这样一个一分为二的对象模型呢?这是因为 HotSopt JVM 的设计者不想让每个对象中都含有一个 vtable(虚函数表),所以就把对象模型拆成 klass 和 oop,其中 oop 中不含有任何虚函数,而 klass 就含有虚函数表,可以进行 method dispatch。这个模型其实是参照的 Strongtalk VM 底层的对象模型。

注:以下C++代码参考自这篇文章

2.OOP是什么?

在 Java 程序运行的过程中,每创建一个新的实例对象,在 JVM 内部就会相应地创建一个对应类型的 oop 对象。oop体系如下:

//定义了oops共同基类
typedef class   oopDesc*                            oop;
//表示一个Java类型实例
typedef class   instanceOopDesc*            instanceOop;
//表示一个Java方法
typedef class   methodOopDesc*                    methodOop;
//表示一个Java方法中的不变信息
typedef class   constMethodOopDesc*            constMethodOop;
//记录性能信息的数据结构
typedef class   methodDataOopDesc*            methodDataOop;
//定义了数组OOPS的抽象基类
typedef class   arrayOopDesc*                    arrayOop;
//表示持有一个OOPS数组
typedef class   objArrayOopDesc*            objArrayOop;
//表示容纳基本类型的数组
typedef class   typeArrayOopDesc*            typeArrayOop;
//表示在Class文件中描述的常量池
typedef class   constantPoolOopDesc*            constantPoolOop;
//常量池告诉缓存
typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class   klassOopDesc*                    klassOop;
//表示对象头
typedef class   markOopDesc*                    markOop;

上面是整个oops模块的组成结构,其中包括多个子模块,每个子模块对应一个类型,每个类型的oop都代表一个在JVM内部使用的特定对象的类型。比如 instanceOopDesc 表示类实例,arrayOopDesc 表示数组。也就是说,当我们用new创建一个Java对象实例的时候,JVM会创建一个 instanceOopDesc 对象来表示这个Java对象。同理,对于数组实例则是 arrayOopDesc。

而各种 oop 类的共同基类为 oopDesc 类:

class oopDesc {
  friend class VMStructs;
  private:
      volatile markOop  _mark; // 对象头
      union _metadata {
        wideKlassOop    _klass;//普通指针
        narrowOop       _compressed_klass;//压缩类指针
      } _metadata;

  private:
      // field addresses in oop
      void*     field_base(int offset)        const;

      jbyte*    byte_field_addr(int offset)   const;
      jchar*    char_field_addr(int offset)   const;
      jboolean* bool_field_addr(int offset)   const;
      jint*     int_field_addr(int offset)    const;
      jshort*   short_field_addr(int offset)  const;
      jlong*    long_field_addr(int offset)   const;
      jfloat*   float_field_addr(int offset)  const;
      jdouble*  double_field_addr(int offset) const;
      address*  address_field_addr(int offset) const;
}


class instanceOopDesc : public oopDesc {
}

class arrayOopDesc : public oopDesc {
}

instanceOopDesc主要包含markOop _mark和union _metadata,实例数据则保存在oopDesc中定义的各种field中。

1.Mark Word:用于存储对象的运行时记录信息,如哈希值、GC 分代年龄(Age)、锁状态标志(偏向锁、轻量级锁、重量级锁)、线程持有的锁、偏向线程 ID、偏向时间戳等。
2.元数据指针OopDesc 中的 _metadata 成员,它是联合体,可以表示未压缩的 Klass 指针(_klass)和压缩的 Klass 指针(narrowOop)。对应的 klass 指针指向一个存储类的元数据的 Klass 对象。

下图是一个对象头的示例:【JVM】底层实现(一):浅谈 OOP-Klass 对象模型

3.Klass是什么?

一个 Klass 对象代表一个类的元数据(与Class对象什么关系呢?向下看…)。klass体系如下:

//klassOop的一部分,用来描述语言层的类型
class  Klass;
//在虚拟机层面描述一个Java类
class   instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class     instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class     instanceRefKlass;
//表示methodOop的Klass
class   methodKlass;
//表示constMethodOop的Klass
class   constMethodKlass;
//表示methodDataOop的Klass
class   methodDataKlass;
//最为klass链的端点,klassKlass的Klass就是它自身
class   klassKlass;
//表示instanceKlass的Klass
class     instanceKlassKlass;
//表示arrayKlass的Klass
class     arrayKlassKlass;
//表示objArrayKlass的Klass
class       objArrayKlassKlass;
//表示typeArrayKlass的Klass
class       typeArrayKlassKlass;
//表示array类型的抽象基类
class   arrayKlass;
//表示objArrayOop的Klass
class     objArrayKlass;
//表示typeArrayOop的Klass
class     typeArrayKlass;
//表示constantPoolOop的Klass
class   constantPoolKlass;
//表示constantPoolCacheOop的Klass
class   constantPoolCacheKlass;

Klass类是其他 klass类型的父类:
【JVM】底层实现(一):浅谈 OOP-Klass 对象模型
JVM在运行时,需要一种用来标识Java内部类型的机制。HotSpot的解决方案是,为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。

  //类拥有的方法列表
  objArrayOop     _methods;
  //描述方法顺序
  typeArrayOop    _method_ordering;
  //实现的接口
  objArrayOop     _local_interfaces;
  //继承的接口
  objArrayOop     _transitive_interfaces;
  //域
  typeArrayOop    _fields;
  //常量
  constantPoolOop _constants;
  //类加载器
  oop             _class_loader;
  //protected域
  oop             _protection_domain;
      ....

Class 对象

问题:Klass(InstanceKlass) 与 Class 对象有什么关系?

我们知道,类数据这样的一个结构体实例也是一个对象,这个对象存在了方法区里。但是这个类数据对象即 InstanceKlass 是给VM内部用的,并不直接暴露给 Java 层。

在 HotSpot VM 里,java.lang.Class 的实例被称为“Java mirror”,意思是它是 VM 内部用的 klass 对象的“镜像”;作用是把 InstanceKlass 对象包装了一层来暴露给 Java 层(开发者)使用,但主要用途是提供反射访问;创建时机时加载->连接->初始化 的初始化阶段。

  • 每个 Java 对象的对象头里,_klass 字段会指向一个VM内部用来记录类的元数据用的 InstanceKlass 对象

  • insanceKlass里有个 _java_mirror 字段(见上),指向该类所对应的Java镜像——java.lang.Class实例

  • 另外,HotSpot VM会给 Class 对象注入一个隐藏字段“klass”,用于指回到其对应的 InstanceKlass 对象。这样,klass与mirror之间就有双向引用,可以来回导航

所以,当我们写 obj.getClass(),在 HotSpot VM 里实际上经过了两层间接引用才能找到最终的 Class 对象

【JVM】底层实现(一):浅谈 OOP-Klass 对象模型

PS:在Oracle JDK7之前,Oracle/Sun JDK 的 HotSpot VM 把Java类的静态变量存在 InstanceKlass 结构的末尾;从Oracle JDK7开始,为了配合 PermGen 移除的工作,Java 类的静态变量被挪到 Java mirror(Class对象)的末尾了。

4.总结

  • JVM 在加载 class 时,会创建 instanceKlass,用来在JVM层表示该Java类,包括常量池、字段、方法等,存放在方法区;
  • 我们在Java代码中 new 一个对象时,JVM会创建一个instanceOopDesc实例来表示这个对象,存放在堆区,其引用,存放在栈区;这个对象中包含了两部分信息,对象头和元数据。对象头有一些运行时的数据,其中就包括跟多线程相关的锁的信息。元数据维护的是指针,指向的是对象所属的类的instanceKlass。
  • HotSpot 并不把 instanceKlass 暴露给 Java,而会另外创建对应的 instanceOopDesc 来表示 java.lang.Class 对象,并将后者称为前者的“Java镜像”,klass 持有指向 oop 引用(_java_mirror 便是该 instanceKlass对Class对象的引用);

下面我们来分析一下,执行 new A() 的时候,JVM native 层里发生了什么?

  1. 首先,如果这个类没有被加载过,JVM 就会进行类的加载,并在 JVM 内部创建一个 instanceKlass 对象表示这个类的运行时元数据,
  2. new A() 时(执行 invokespecial A::),JVM 就会创建一个 instanceOopDesc 对象表示这个对象的实例,然后进行 Mark Word 的填充,将元数据指针指向 Klass 对象(KlassPointer),并填充实例变量。

==> 结合JVM分析以下代码执行过程:

public class Test{
    public static void main(String[] args) {
      Person person = new Person();
      person.setName(“张三”);
    }
}
  1. 编译并运行 Test.Class,随后被加载到JVM的元空间,在那存储着类的信息(包括类的名称、方法信息、字段信息…)。
  2. JVM找到 Test 的主函数入口(main),为 main 函数创建栈帧,开始执行 main 函数。
  3. main函数的第一条语句是 Person person = new Person(),就是让JVM创建一个 Person 对象,但是这时候方法区中没有 Person 类的信息,所以JVM马上加载 Person 类,把 Person 类的类型信息放到方法区中(元空间),即创建 instanceKlass 对象;同时,在堆上会自动构造一个镜像的 Class 对象提供反射访问(它和 instanceKlass 持有着双向引用)。
  4. 加载完 Person 类之后,JVM 会为一个新的 Person 实例分配内存, 然后调用构造函数初始化 person 实例,即在堆上创建一个 instanceOopDesc 实例,并返回该实例的指针OOP到栈空间对应线程。
  5. 当执行 person.setName("张三")的时候,JVM 首先根据 person 引用找到 person 对象;因为这个 person 实例持有着指向方法区的 Person 类的类型信息的引用(其中包含有方法表,java动态绑定的底层实现等),所以接下来,通过 KlassPointer 定位到方法区中 Person 类的类型信息的方法表,获得setName()函数的字节码的地址。
  6. 根据方法表中内容,在虚拟机栈为setName()函数创建栈帧,开始运行setName()函数。
上一篇:【JVM】Java对象的内存布局


下一篇:Java Web 基础篇 L1 关于代理