最近对 JVM 兴趣大增(其实是想回归C艹的怀抱了)~
当我们在写 Java 代码的时候,我们会面对着无数个接口,类,对象和方法。但我们有木有想过,Java 中的这些对象、类和方法,在 HotSpot JVM 中的结构又是怎么样呢?HotSpot JVM 底层都是 C++ 实现的,那么 Java 的对象模型与 C++ 对象模型之间又有什么关系呢?今天就来分析一下 HotSpot JVM 中的对象模型:oop-klass model,它们的源码位于 openjdk-8/openjdk/hotspot/src/share/vm/oops 文件夹内。
注:本文对应的 OpenJDK 版本为 openjdk-8u76-b02。对于不同的版本(openjdk-7, openjdk-8, openjdk-9),其对应的 HotSpot JVM 的对象模型有些许差别(7和8的差别比较大)
oop-klass model 概述
HotSpot JVM 并没有根据 Java 实例对象直接通过虚拟机映射到新建的 C++ 对象,而是设计了一个 oop-klass model。
当时第一次看到 oop,我的第一反应就是 object-oriented programming,其实这里的 oop 指的是 Ordinary Object Pointer(普通对象指针),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象。而 klass 则包含 元数据和方法信息,用来描述 Java 类。
那么为何要设计这样一个一分为二的对象模型呢?这是因为 HotSopt JVM 的设计者不想让每个对象中都含有一个 vtable(虚函数表),所以就把对象模型拆成 klass 和 oop,其中 oop 中不含有任何虚函数,而 klass 就含有虚函数表,可以进行 method dispatch。这个模型其实是参照的 Strongtalk VM 底层的对象模型。
体系总览
在 oopsHierarchy.hpp 里定义了 oop 和 klass 各自的体系。
这是oop的体系:
1 2 3 4 5 |
typedef class oopDesc* oop; typedef class instanceOopDesc* instanceOop; typedef class arrayOopDesc* arrayOop; typedef class objArrayOopDesc* objArrayOop; typedef class typeArrayOopDesc* typeArrayOop; |
注意由于 Java 8 引入了 Metaspace,OpenJDK 1.8 里对象模型的实现与 1.7 有很大的不同。原先存于 PermGen 的数据都移至 Metaspace,因此它们的 C++ 类型都继承于 MetaspaceObj 类(定义见 vm/memory/allocation.hpp),表示元空间的数据。
这是元数据的体系:
1 2 3 4 5 6 7 8 9 10 11 |
// The metadata hierarchy is separate from the oop hierarchy
// class MetaspaceObj |
这是 klass 的体系:
1 2 3 4 5 6 7 8 9 10 |
// The klass hierarchy is separate from the oop hierarchy.
class Klass; |
注意 klass 代表元数据,继承自 Metadata 类,因此像 Method、ConstantPool 都会以成员变量(或指针)的形式存在于 klass 体系中。
以下是 JDK 1.7 中的类在 JDK 1.8 中的存在形式:
- klassOop -> Klass*
- klassKlass 不再需要
- methodOop -> Method*
- methodDataOop -> MethodData*
- constMethodOop -> ConstMethod*
- constantPoolOop -> ConstantPool*
- constantPoolCacheOop -> ConstantPoolCache*
klass
一个 Klass 对象代表一个类的元数据(相当于 java.lang.Class 对象)。它提供:
- language level class object (method dictionary etc.)
- provide vm dispatch behavior for the object
所有的函数都被整合到一个 C++ 类中。
Klass 对象的继承关系:xxxKlass <:< Klass <:< Metadata <:< MetaspaceObj
klass 对象的布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Klass layout: // [C++ vtbl ptr ] (contained in Metadata) // [layout_helper ] // [super_check_offset ] for fast subtype checks // [name ] // [secondary_super_cache] for fast subtype checks // [secondary_supers ] array of 2ndary supertypes // [primary_supers 0] // [primary_supers 1] // [primary_supers 2] // ... // [primary_supers 7] // [java_mirror ] // [super ] // [subklass ] first subclass // [next_sibling ] link to chain additional subklasses // [next_link ] // [class_loader_data] // [modifier_flags] // [access_flags ] // [last_biased_lock_bulk_revocation_time] (64 bits) // [prototype_header] // [biased_lock_revocation_count] // [_modified_oops] // [_accumulated_modified_oops] // [trace_id] |
oop
oop 类型其实是 oopDesc*。在 Java 程序运行的过程中,每创建一个新的对象,在 JVM 内部就会相应地创建一个对应类型的 oop 对象。各种 oop 类的共同基类为 oopDesc 类。
在 JVM 内部,一个 Java 对象在内存中的布局可以连续分成两部分:instanceOopDesc 和实例数据。instanceOopDesc 和 arrayOopDesc 又称为 对象头。
instanceOopDesc 对象头包含两部分信息:Mark Word 和 元数据指针(Klass*):
1 2 3 4 5 |
volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; |
分别来看一下:
- Mark Word:instanceOopDesc 中的 _mark 成员,允许压缩。它用于存储对象的运行时记录信息,如哈希值、GC 分代年龄(Age)、锁状态标志(偏向锁、轻量级锁、重量级锁)、线程持有的锁、偏向线程 ID、偏向时间戳等。
- 元数据指针:instanceOopDesc 中的 _metadata 成员,它是联合体,可以表示未压缩的 Klass 指针(_klass)和压缩的 Klass 指针。对应的 klass 指针指向一个存储类的元数据的 Klass 对象。
下面我们来分析一下,执行 new A() 的时候,JVM native 层里发生了什么。首先,如果这个类没有被加载过,JVM 就会进行类的加载,并在 JVM 内部创建一个 instanceKlass 对象表示这个类的运行时元数据(相当于 Java 层的 Class 对象)。到初始化的时候(执行 invokespecial A::<init>),JVM 就会创建一个 instanceOopDesc 对象表示这个对象的实例,然后进行 Mark Word 的填充,将元数据指针指向 Klass 对象,并填充实例变量。
根据对 JVM 的理解,我们可以想到,元数据—— instanceKlass 对象会存在元空间(方法区),而对象实例—— instanceOopDesc 会存在 Java 堆。Java 虚拟机栈中会存有这个对象实例的引用。
参考文档
本文标题:深入探究 JVM | klass-oop对象模型研究
文章作者:sczyh30
发布时间:2016年01月06日
原始链接:http://www.sczyh30.com/posts/Java/jvm-klass-oop/
许可协议: "知识共享-保持署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。
文章评论