Java对象模型

oop-klass模型

Hotspot 虚拟机在内部使用两组类来表示Java的类和对象。

oop(ordinary  object  pointer),用来描述对象实例信息。

klass,用来描述 Java 类,是虚拟机内部Java类型结构的对等体 。

JVM内部定义了各种oop-klass,在JVM看来,不仅Java类是对象,Java 方法也是对象, 字节码常量池也是对象,一切皆是对象。JVM使用不同的oop-klass模型来表示各种不同的对象。 而在技术落地时,这些不同的模型就使用不同的 oop 类和 klass 类来表示 。由于JVM使用C/C++编写,因此这些 oop 和 klass 类便是各种不同的C++类。对于Java类型与实例对象,只叫使用 instanceOop 和 instanceKlass 这 2 个 C++类来表示。

描述HotSpot中的oop 体系


也许是为了简化变量名,JVM统一将最后的Desc去掉,全部处理成以 Oop 结尾的类型名。 例如对于 Java 类中所定义的方法 ,只明使用 methodOop 去描述 Java 方法的全部信息;对于 Java 类中所定义的引用对象变量 ,JVM则使用objArrayOop来保存这个引用变量的 “全息”信息。


纵观以上oop和 klass 体系的定义,可以发现,无论是 oop 还是 klass ,基本都被划分为来分别描述 instance 、method 、constantMethod 、methodData 、array 、objArray 、typeArray 、constantPool 、 constantPoolCache 、klass 、compoiledICHolder这几种模型,这几种模型中的每一种都有一个对应的 xxxOopDesc 和对应的 xxxKlass 。通俗而言,这几种模型分别用于描述 Java 类类型和类型指针 、Java   方法类型和方法指针 、常量池类型及指针 、基本数据类型的数组类型及指针 、引用类型的数组类型及指针 、常量池缓存类型及指针、Java类实例对象类型及指针。Hotspot认为使用这几种模型 ,便足以勾画Java程序的全部 :数据、方法 、类型 、数组和实例。

那么oop到底是啥,其存在的意义究竟是什么?其名称已经说得很清楚,就是普通对象指 针。指针指向哪里?指向 klass 类实例。直接这么说可能比较难以理解,举个例子,若 Java 程序中定义了一个类 ClassA ,同时程序中有如下代码 :

Class a = new ClassA ( );  

当Hotspot执行到这里时,会先将 ClassA 这个类型加载到 perm 区 ( 也叫方法区 ),然后在 Hotspot 堆中为其实例对象a开辟一块内存空间,存放实例数据。在 JVM加载ClassA到 perm 区时,JVM就会创建一个instanceKlass,instanceKlass中保存了 ClassA 这个 Java 类中所定义的一切信息,包括变量 、方法 、父类 、接 口、构造函数 、属性等,所以 instanceKlass 就是 ClassA这个Java类类型结构的对等体。而 instanceOop  这个“普通对象指针”对象中包含了一个指针,该指针就指向instanceKlass这个实例。在JVM实例化ClassA时,JVM又会在堆中创建一个instanceOop , instanceOop便是 ClassA 对象实例 a 在内存中的对等体,主要存储 ClassA 实例对象的成员变量。 其中,instanceOop 中有一个指针指向 instanceKlass ,通过这个指针,JVM便可以在运行期获取这个类实例对象的类元信息。

oopDesc

既然讲到了oop,就不得不提 JVM中所有oop对象的老祖宗oopDesc类。上述列表里的所有 oopDesc ,诸如 instanceOopDesc 、constantPoolOopDesc 、klassOopDesc 等 ,在 C++的继承体系中,最终全都来自顶级的父类oopDesc ( JDK8中已经没有 oopDesc ,换成了别的名字,但是换汤不换药,内部结构并没有什么太大的变化)。


抛开友元类VMStructs,以及用于内存屏障的_bs , oopDesc类中只剩下了2 个成员变量( 友元类并不算成员变量 ):mark 和 metadata。其中 metadata 是联合结构体,里面包含两个元素 ,分别是 wideKlassOop 与 narrowOop,顾名思义,前者是宽指针,后者是压缩指针。关于宽指针与窄指针这里先简单提一句,主要用于JVM是否对Java class进行压缩,如果使用了压缩技术, 自然可以节省出一定的宝贵内存空间。

oopDesc的这 2 个成员变量的作用很简单,_mark顾名思义,似乎是一种标记,而事实上也的确如此,Java 类在整个生命周期中,会涉及到线程状态 、并发锁 、GC 分代信息等内部标识,这些标识全都打在_mark变量上。而 _metadata顾名思义也很简单,用于标识元数据。每一个 Java 类都会包含一定的变量 、方法 、父类 、所实现的接口等信息,这些均可称为 Java 类的“元数据”,其实可以更加通俗点,所谓的元数据就是在前面反复讲的数据结构。Java类的结构信息在编译期被编译为字节码格式,JVM则在运行期进一步解析字节码格式,从字节码二进制流中还原出一个Java在源码期间所定义的全部数据结构信息,JVM需要将解析出来结果保存到内存中,以便在运行期进行各种操作,例如反射,而_metadata便起到指针的作用,指向 Java 类的数据结构被解析后所保存的内存位置。

仍然以上一节所举的实例化ClassA这个自定义 Java 类的例子进行说明。当JVM完成ClassA类型的实例化之后,会为该 Java 类创建对应的 oop-klass 模型 ,oop 对应的类是 instanceOop ,klass 对应的类是 instanceKlass 。上一节讲过 ,instanceOop 内部会有一个指针指向 instanceKlass ,其实这个指针便是 oopDesc 中所定义的一_metadata。klass 是 Java类型的对等体 ,而 Java 类型 ,便是 Java 编程语言中用于描述客观事物的数据结构,而数据结构包含一个客观事物的全部属性和行为 ,所以叫做 “类元”信息,这便是_metadata的本意。

_metadata的作用可以参考下图所示。


两模型三维度

前文讲过,JVM内部基于oop-klass模型描述一个 Java 类 ,将一个 Java 类一拆为二分别描述,第一个模型是oop,第二个模型是klass。所谓oop,并不是object-oriented programming(面向对象编程),而是ordinary object pointer(普通对象指针),它用来表示对象的实例信息,看起来像个指针,而实际上对象实例数据都藏在指针所指向的内存首地址后面的一片内存区域中。    

而klass则包含元数据和方法信息,用来描述 Java 类而 klass 则包含元数据和方法信息,用来描述 Java 类或者JVM内部自带的C++类型信息。其实,klass便是前文一直在讲的数据结构,Java 类的继承信息、成员变量 、静态变量 、成员方法 、构造函数等信息都在 klass 中保存 ,JVM据此便可以在运行期反射出Java类的全部结构信息。当然,JVM本身所定义的用于描述Java类的C++类也使用klass去描述,这相当于使用另一种面向对象的机制去描述C++类这种本身便是面向对象的数据。

JVM使用 oop-klass 这种一分为二的模型描述一个 Java 类 ,虽然模型只有两种,但是其实从 3 个不同的维度对一个 Java 类进行了描述。侧重于描述 Java 类的实例数据的第一种模型 oop 主要为 Java 类生成一张 “实例数据视图”,从数据维度描述一个Java类实例对象中各个属性在运行期的值。而第二种模型 klass 则又分别从两个维度去描述一个 Java 类 ,第一个维度是 Java 类的“元信息视图”,另一个维度则是虚函数列表,或者叫作方法分发规则。元信息视图为JVM在运行期呈现Java类的“全息”数据结构信息,这是JVM在运行期得以动态反射出类信息的基础。

下面的图描述了JVM内部对Java类的 “两模型三维度” 的映射。


体系总览

在JVM内部定义了3种结构去描述一种类型 :oop 、klass 和 handle 类。注意,这 3 种数据结构不仅能够描述外在的 Java 类 ,也能够描述 JVM内在的C++类型对象。

前面讲过,klass主要描述 Java 类和 JVM内部C++类型的元信息和虚函数,这些元信息的实际值就保存在oop里面。oop 中保存一个指针指向 klass ,这样在运行期JVM便能够知道每一个实例的数据结构和实际类型。handle是对 oop 的行为的封装,在访问 Java 类时一定是通过 handle 内部指针得到 oop 实例的,再通过 oop 就能拿到 klass ,如此 handle 最终便能操纵 oop 的行为了(注意,如果是调用JVM内部C++类型所对应的oop的函数 ,则不需要通过 handle 来中转,直接通过 oop 拿到指定的 klass便能实现)。klass 不仅包含自己所固有的行为接口,而且也能够操作 Java 类的函数。由于Java 函数在JVM内部都被表示成虚函数,因此handle模型其实就是 Java  类行为的表达。

先上一张图说明这种三角关系。


可以看到,Handle类内部只有一个成员变量一handle,该变量类型是oop*,因此该变量最终指向的就是一个oop的首地址。换言之,只要能够拿到 Handle 对象,便能据此得到其所指向的 oop 对象实例,而通过oop 对象实例又能进一步获取其所关联的 klass 实例,而获取到 klass 对象实例后,便能实现对oop对象方法的调用。因此,虽然从表面上看,handle体系貌似是对 oop 的一种封装 ,但是实际上其醉翁之意在于最终的 klass 体系。

oop一般由对象头、对象专有属性和数据体这 3 部分构成。其一般结构如图所示。


oop体系

所谓oop,就是ordinary object pointer ,也即普通对象指针。但是究竟什么才是普通对象指针呢?要搞清楚何谓 oop ,要问2个问题:

1 ) Hotspot里的 oop 指啥

Hotspot里的oop 其实就是 GC 所托管的指针,每一个 oop 都是一种 xxxOopDesc*类型的指针。所有oopDesc及其子类( 除神奇的 markOopDesc 外 ) 的实例都由 GC 所管理,这才是最最重要的,是 oop 区分 Hotspot 里所使用的其他指针类型的地方。

2)对象指针之前为何要冠以“普通”二字

对象指针从本质上而言就是一个指针,指向xxxOopDesc的指针也是普通得不能再普通的 指针,可是为何在 Hotspot 领域还要加一个“普通”来修饰?要回答这个问题,需要追溯到OOP( 这里的OOP 是指面向对象编程 )的鼻祖SmallTalk 语言。

SmallTalk语言里的对象也由 GC 来管理,但是 SmallTalk 里面的一些简单的值类型对象都 会使用所谓的 “直接对象”的机制来实现,例如SmallTalk里面的整数类型。所谓 “直接对象”( immediate object) 就是并不在 GC 堆上分配对象实例,而是直接将实例内容存在对象指针里的对象。这样的指针也叫做 “带标记的指针”(tagged pointer)。

这一点倒是与markOopDesc类型如出一辙,因为 markOopDesc 也是将整数值直接存储在指针里面 ,这个指针实际上并无“指向”内存的功能。

所以在SmallTalk的运行期 ,每当拿到一个对象指针时,都得先校验这个对象指针是一个直接对象还是一个真的指针?如果是真的指针,它就是一个“普通”的对象指针了。这样对象指针就有了“普通”与“不普通”之分。

所以,在Hotspot里面 ,oop 就是指一个真的指针,而 markOop 则是一个看起来像指针但实际上是藏在指针里的对象(数据)。这也正是 markOop 实例不受 GC 托管的原因,因为只要出了函数作用域,指针变量就会直接被从堆枝上释放掉了不需要垃圾回收了。


klass体系

oop的讲述先告一段落 ,再来看看 klass 部分。按照JVM的官方解释,klass主要提供下面2种能力 :

©klass提供一个与 Java 类对等的 C++类型描述。

©klass提供虚拟机内部的函数分发机制 。

其实这种说法与上文所说的2种维度的含义是相同的。klass 分别从类结构和类行为这两方面去描述一个 Java 类 ( 当然也包含JVM内部非开放的C++类)。

与oop相同,在JVM内部也不是klass一个人在战斗,而是一个家族。klass 家族体系如下:


handle体系

前面讲过,handle封装了oop,由于通过oop可以拿到 klass ,而 klass 是对 Java 类数据结构和方法的描述 ,因此 handle 间接封装了 klass。JVM内部使用一个 table 来存储 oop 指针。

如果说oop是对普通对象的直接引用,那么 handle 就是对普通对象的一种间接引用,中间隔了一层。但是JVM内部为何要使用这种间接引用呢?答案是,这完全是为GC考虑。具体表现在2个地方 :

通过handle,能够让 GC 知道其内部代码都有哪些地方持有 GC 所管理的对象的引用,这只需要扫描 handle 所对应的 table ,这样 JVM 便无须关注其内部到底哪些地方持有对普通对象的引用。

在GC过程中如果发生了对象移动(例如从新生代移到了老年代),那么JVM的内部引用无须跟着更改为被移动对象的新地址,JVM 只需要更改 handle table 里对应的指针即可 。

当然实际的handle作为对 Java 类方法的访问的包装,远不止上面所描述的这么简单。这里涉及 Java 类的类继承和接口继承的话题,在 C++领域,类的继承和多态性最终通过vptr(虚函数表)来实现。在klass内部,记录了每一个类的vptr信息,具体而言分为两部分来描述。

1.vtable虚函数表

vtable中存放 Java 类中非静态和非 private 的方法入口,JVM调用 Java 类的方法 (非静态和非 private)时,最终会访问vtable,找到对应的方法入口。

2.itable 接口函数表

itable中存放 Java 类所实现的接口类方法。同样,JVM调用接口方法时,最终会访问itable,找到对应的接口方法入口。

不过要注意,vtable和itable 里面存放的并不是Java类方法和接口方法的直接入口,而是指向了 Method 对象入口,JVM会通过Method最终拿到真正的 Java 类方法入口,得到方法所对应的字节码/二进制机器码并执行。当然,对于被JIT进行动态编译后的方法,JVM最终拿到的是其对应的被编译后的本地方法的入口。


这里有个问题,前面不是一直在说handle是对 oop 的直接封装和对 klass 的间接封装吗,为什么这里却分别给 oop 和 klass 定义了 2 套不同的 handle 体系呢?这给人的感觉好像是,封 装 oop 的 handle 和封装 klass 的 handle 并不是同一个 handle ,既然不是同一个handle ,那么通 过封装 oop 的handle 还怎么去得到所对应的 klass 信息呢?

其实这正是只怕内部常常容易使人迷惑的地方。在JVM中,使用oop-klass这种一分为二的模型去描述 Java 类以及 只叫内部的特殊类群体,为此JVM内部特定义了各种oop和 klass类型。但是,对于每一个oop,其实都是一个 C++类型,也即 klass;而对于每一个 klass 所对应的 class ,在JVM内部又都会被封装成 oop。只怕在具体描述一个类型时,会使用 oop 去存储这个类型的实例数据,并使用 klass 去存储这个类型的元数据和虚方法表。而当一个类型完成其生命周期后,JVM会触发 GC 去回收,在回收时,既要回收一个类实例所对应的实例数据 oop , 也要回收其所对应的元数据和虚方法表(当然,两者并不是同时回收,一个是堆区的垃圾回收, 一个是永久区的垃圾回收)。为了让 GC 既能回收 oop 也能回收 klass,因此 oop 本身被封装成了 oop ,而 klass 也被封装成 oop。而只叫内部恰好将描述类实例的 oop 全都定义成类名以 oop 结尾的类,并将描述类结构和方法信息的 klass 全都定义成类名以 klass 结尾的类 ,而只怕内部描述类信息的模型恰巧也叫作 oop-klass,与类名存在重合,这就导致了很多人的疑惑,这些疑惑完全是因为叫法上的重合而产生。

因此为了进一步解开疑惑,我们不妨换个叫法,不再将JVM内部描述类信息的模型叫作

oop-klass,而是叫作 data-meta 模型 (瞎取的名字没啥特殊含义)。然后将JVM内部的 oop 体系的类名全都改成以 Data结尾 ,例如,methodData 、instanceData 、constantPoolData 等,同时 将 klass 体系的类名也全都改成以 Meta 结尾,例如methodMeta 、instanceMeta 、constantPoolMeta 等。JVM在进行 GC 时,既要回收 Data 类实例,也要回收 Meta 类实例,为了让 GC 便于回收,因此对于每一个 Data 类和每一个 Meta 类 ,JVM在内部都将其封装成了 oop 模型。对于 Data 类,其内存布局是前面为 oop 对象头 ,后面紧跟实例数据;而对 Meta 类 ,其内存布局是前面为 oop 对象头,后面紧跟实例数据和虚方法表。封装成 oop 之后,再进一步使用 handle 来封装, 于是便有利于 GC 内存回收。

在这种新的模型中,不管是Data类还是 Meta 类,都是一种普通的 C++类型,只不过它们从不同的角度对 Java 类进行了描述。不管是 Data 类还是 Meta 类,当其所在的JVM的内存区域爆满后,都会触 GC,为了方便回收,因此就需要将其封装成 oop。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容