深入探究 JVM | klass-oop 对象模型研究

1,821 阅读4分钟

最近对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的体系:

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),表示元空间的数据。

这是元数据的体系:


class   ConstMethod;
class   ConstantPoolCache;
class   MethodData;
class   Method;
class   ConstantPool;
class   CompiledICHolder;

这是klass的体系:


class Klass;
class   InstanceKlass;
class     InstanceMirrorKlass;
class     InstanceClassLoaderKlass;
class     InstanceRefKlass;
class   ArrayKlass;
class     ObjArrayKlass;
class     TypeArrayKlass;

注意klass代表元数据,继承自Metadata类,因此像MethodConstantPool都会以成员变量(或指针)的形式存在于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对象的布局如下:

//  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和实例数据。instanceOopDescarrayOopDesc又称为对象头。

instanceOopDesc对象头包含两部分信息:Mark Word元数据指针(Klass*):

volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  

分别来看一下:

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

下面我们来分析一下,执行new A()的时候,JVM Native里发生了什么。首先,如果这个类没有被加载过,JVM就会进行类的加载,并在JVM内部创建一个instanceKlass对象表示这个类的运行时元数据(相当于Java层的Class对象。到初始化的时候(执行invokespecial A::),JVM就会创建一个 instanceOopDesc对象表示这个对象的实例,然后进行Mark Word的填充,将元数据指针指向Klass对象,并填充实例变量。

根据对JVM的理解,我们可以想到,元数据—— instanceKlass 对象会存在元空间(方法区),而对象实例—— instanceOopDesc 会存在Java堆。Java虚拟机栈中会存有这个对象实例的引用。

参考文档