Java对象模型-oop和klass

oop-klass模型

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

  • oop(ordinary  object  pointer),用来描述对象实例信息。
  • klass,用来描述 Java 类,是虚拟机内部Java类型结构的对等体 。

JVM内部定义了各种oop-klass,在JVM看来,不仅Java类是对象,Java 方法也是对象, 字节码常量池也是对象,一切皆是对象。JVM使用不同的oop-klass模型来表示各种不同的对象。 而在技术落地时,这些不同的模型就使用不同的 oop 类(instanceoop  methodoop constmethodoop等等)和 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(普通对象指针),它用来表示对象的实例信息,看起来像个指针,而实际上对象实例数据都藏在指针所指向的内存首地址后面的一片内存区域中。   (理解:oop指向堆中实例对象所在内存区域的首地址) 

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。


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/439812.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Apollo进阶课程㉔丨Apollo 规划技术详解——Motion Planning Environment

原文链接:进阶课程㉔丨Apollo 规划技术详解——Motion Planning Environment 自动驾驶汽车核心技术包括环境感知、行为决策、运动规划与控制等方面。其中,行为决策系统、运动规划与控制系统作为无人驾驶汽车的“大脑”,决定了其在不同交通驾…

一步步编写操作系统 26 打开A20地址线

打开A20地址线 还记得实模式下的wrap-around吗?也就是地址回绕。咱们一起来复习一下。实模式下内存访问是采取“段基址:段内偏移地址”的形式,段基址要乘以16后再加上段内偏移地址。实模式下寄存器都是16位的,如果段基址和段内偏移地址都为1…

一步步编写操作系统 27 处理器微架构之流水线简介

了解处理器内部硬件架构,有助于理解软件运行原理,因为这两者本身相辅相成,相互依存。就像枪和狙击手,枪的操作和外形设计都是要根据人体工学,让人不仅操作容易,而且携带也要轻便,做到能随时射出…

Apollo进阶课程㉚丨Apollo ROS背景介绍

原文链接:进阶课程㉚丨Apollo ROS背景介绍 ROS是机器人学习和无人车学习最好Linux平台软件,资源丰厚。无人车的规划、控制算法通常运行在Linux系统上,各个模块通常使用ROS进行连接。 上周阿波君为大家详细介绍了「进阶课程㉙Apollo控制技术详…

一步步编写操作系统 30 cpu的分支预测简介

人在道路的分岔口时要预测哪条路能够到达目的地,面对众多选择时,计算机也一样要抉择,毕竟计算机的运行方式是以人的思路来设计的,计算机中的抉择其实就是人在抉择。 cpu中的指令是在流水线上执行。分支预测,是指当处理…

【HDU - 5492】Find a path(dp,tricks)

题干: Frog fell into a maze. This maze is a rectangle containing NN rows and MM columns. Each grid in this maze contains a number, which is called the magic value. Frog now stays at grid (1, 1), and he wants to go to grid (N, M). For each step,…

Apollo进阶课程㉜丨Apollo ROS原理—1

原文链接:进阶课程㉜丨Apollo ROS原理—1 ROS在开发过程中,基于功能把整个自动驾驶系统分成多个模块,每个模块负责自己消息的接收、处理、发布。当模块需要联调时,通过框架可以把各个模块快速的集成到一起。 上周阿波君为大家详细…

Ubuntu下安装Chrome浏览器的两个方法

一、通过直接下载安装Google Chrome浏览器deb包。 打开Ubuntu终端,以下为32位版本,使用下面的命令。 wget https://dl.google.com/linux/direct/google-chrome-stable_current_i386.deb 以下为64位版本,使用下面的命令。 wget https://dl.…

Apollo进阶课程㉝丨Apollo ROS原理—2

原文链接:进阶课程㉝丨Apollo ROS原理—2 在ROS系统中,从数据的发布到订阅节点之间需要进行数据的拷贝。在数据量很大的情况下,很显然这会影响数据的传输效率。所以Apollo项目对于ROS第一个改造就是通过共享内存来减少数据拷贝,以…

Java 10 常用集合继承关系图

概述 集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用。 类图如下: 1、Iterable与Iterator接口之间的区别 我看到好多网上的文章类图里面Collection 是继承Iterator接口&a…

【CodeForces - 673D】Bear and Two Paths(构造,tricks)

题干: Bearland has n cities, numbered 1 through n. Cities are connected via bidirectional roads. Each road connects two distinct cities. No two roads connect the same pair of cities. Bear Limak was once in a city a and he wanted to go to a cit…

Apoll进阶课程㉞丨Apollo ROS原理—3

原文链接:进阶课程㉞丨Apollo ROS原理—3 机器人操作系统(ROS)是一个成熟而灵活的机器人编程框架。ROS提供了所需的工具,可以轻松访问传感器数据,处理数据,并为机器人的电机和其它执行器生成适当的响应。整个ROS系统被设计为在计…

SM3密码杂凑算法原理

目录 1.概述 2、算法描述 2.1 概述 2.2 填充 2.3 迭代压缩 2.3 消息扩展 2.4 压缩函数 2.5 杂凑值 1.概述 SM3是我国采用的一种密码散列函数标准,由国家密码管理局于2010年12月17日发布。相关标准为“GM/T 0004-2012 《SM3密码杂凑算法》”。 在商用密码体…

动手学无人驾驶(1):交通标志识别

今天主要介绍无人驾驶当中深度学习技术的应用。 本文是根据博客专家AdamShan的文章整理而来,在此表示感谢。 关于深度学习的图像分类技术,网上已有很多关于深度学习的课程(如吴恩达老师的深度学习专项课程),故本文不对…

《操作系统真象还原》-阅读笔记(上)

第一章 配置bochs,进入bochs simulator后一直是黑屏,原来默认是调试模式,需要输入C(continue)来让调试继续。 第二章 主讲MBR及进入MBR前的步骤 1.实模式只能访问1MB的内存空间。 2.BIOS在ROM中。 3.开机上电后CS&a…

Apollo进阶课程㉟丨Apollo ROS原理—4

原文链接:进阶课程㉟丨Apollo ROS原理—4 ROS是一个强大而灵活的机器人编程框架,从软件构架的角度说,它是一种基于消息传递通信的分布式多进程框架。 ROS本身是基于消息机制的,可以根据功能把软件拆分成为各个模块,每…

《操作系统真象还原》-阅读笔记(中)

第七章 操作系统是由中断驱动的。 中断分为外部中断和内部中断。 外部中断分为可屏蔽中断和不可屏蔽中断,内部中断分为软中断和异常。 外部中断 来自CPU外部的中断。可屏蔽中断:通过INTR引脚进入CPU,外部设备如硬盘、网卡、打印机等发出的…

动手学无人驾驶(2):车辆检测

上一篇博客介绍了无人驾驶中深度学习在交通标志识别中的应用(动手学无人驾驶(1):交通标志识别)。 本文介绍如何使用深度学习进行车辆检测,使用到的模型是YOLO模型,关于YOLO模型的具体检测原理&a…

《操作系统真象还原》-阅读笔记(下)

第十一章 任意进程的页目录表第0~767个页目录项属于用户空间,指向用户页表。第768~1023个页目录项指向内核页表。每创建一个新的用户进程,就将内核页目录项复制到用户进程的页目录表,其次需要把用户页目录表中最后一个页目录项更新为用户进程自己的页目…

Apollo进阶课程㊱丨Apollo ROS深入介绍

原文链接:进阶课程㊱丨Apollo ROS深入介绍 ROS是一个强大而灵活的机器人编程框架,从软件构架的角度说,它是一种基于消息传递通信的分布式多进程框架。ROS本身是基于消息机制的,可以根据功能把软件拆分成为各个模块,每…