当我们在写 Java 代码的时候,我们会面对着无数个接口,类,对象和方法。但我们有木有想过,Java 中的这些对象、类和方法,在 HotSpot JVM 中的结构又是怎么样呢?HotSpot JVM 底层都是 C++ 实现的,那么 Java 的对象模型与 C++ 对象模型之间又有什么关系呢?今天就来分析一下 HotSpot JVM 中的对象模型:oop-klass model。
HotSpot JVM 并没有根据 Java 实例对象直接通过虚拟机映射到新建的 C++ 对象,而是设计了一个 oop-klass 模型。oop 指的是 Ordinary Object Pointer(普通对象指针,指向被创建的对象,具体通过什么结构创建对象请向下看…),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象;而 klass 则包含元数据和方法信息,用来描述 Java 类。
那么为何要设计这样一个一分为二的对象模型呢?这是因为 HotSopt JVM 的设计者不想让每个对象中都含有一个 vtable(虚函数表),所以就把对象模型拆成 klass 和 oop,其中 oop 中不含有任何虚函数,而 klass 就含有虚函数表,可以进行 method dispatch。这个模型其实是参照的 Strongtalk VM 底层的对象模型。
注:以下C++代码参考自这篇文章…
在 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 对象。
下图是一个对象头的示例:
一个 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在运行时,需要一种用来标识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 对象
PS:在Oracle JDK7之前,Oracle/Sun JDK 的 HotSpot VM 把Java类的静态变量存在 InstanceKlass 结构的末尾;从Oracle JDK7开始,为了配合 PermGen 移除的工作,Java 类的静态变量被挪到 Java mirror(Class对象)的末尾了。
下面我们来分析一下,执行 new A() 的时候,JVM native 层里发生了什么?
首先,如果这个类没有被加载过,JVM 就会进行类的加载,并在 JVM 内部创建一个 instanceKlass 对象表示这个类的运行时元数据,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(“张三”); } } 编译并运行 Test.Class,随后被加载到JVM的元空间,在那存储着类的信息(包括类的名称、方法信息、字段信息…)。JVM找到 Test 的主函数入口(main),为 main 函数创建栈帧,开始执行 main 函数。main函数的第一条语句是 Person person = new Person(),就是让JVM创建一个 Person 对象,但是这时候方法区中没有 Person 类的信息,所以JVM马上加载 Person 类,把 Person 类的类型信息放到方法区中(元空间),即创建 instanceKlass 对象;同时,在堆上会自动构造一个镜像的 Class 对象提供反射访问(它和 instanceKlass 持有着双向引用)。加载完 Person 类之后,JVM 会为一个新的 Person 实例分配内存, 然后调用构造函数初始化 person 实例,即在堆上创建一个 instanceOopDesc 实例,并返回该实例的指针OOP到栈空间对应线程。当执行 person.setName("张三")的时候,JVM 首先根据 person 引用找到 person 对象;因为这个 person 实例持有着指向方法区的 Person 类的类型信息的引用(其中包含有方法表,java动态绑定的底层实现等),所以接下来,通过 KlassPointer 定位到方法区中 Person 类的类型信息的方法表,获得setName()函数的字节码的地址。根据方法表中内容,在虚拟机栈为setName()函数创建栈帧,开始运行setName()函数。