当我们在写 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 对象。
下图是一个对象头的示例:
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在运行时,需要一种用来标识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对象)的末尾了。
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 层里发生了什么?
- 首先,如果这个类没有被加载过,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()
函数。