HotSpot的Java对象模型:Oop-Klass模型

Java中Object类是所有对象的父类,这是语言层面的定义。在JVM层面,不仅Java类是对象,Java 方法也是对象, 字节码常量池也是对象,一切皆是对象。JVM使用不同的oop-klass模型来表示各种不同的对象。 而在技术落地时,这些不同的模型就使用不同的 oop 类(instanceoop  methodoop constmethodoop等等)和 klass 类来表示 。由于JVM使用C/C++编写,因此这些 oop 和 klass 类便是各种不同的C++类。对于Java类型与实例对象,只叫使用 instanceOop 和 instanceKlass 这 2 个 C++类来表示。refrence、oop、klass的关系如下所示:

Oop继承体系

oop(ordinary object pointer,普通对象指针)。是HotSpot用来表示Java对象的实例信息的一个体系,既在JVM层面,oop用于表示对象(oop本质上是一个指向内存中对象的起始存储位置的指针)。在hotspot/share/oops/oopsHierarchy.hpp 文件中,对oop的定义如下:

typedef class oopDesc*                    oop;
typedef class   instanceOopDesc*            instanceOop;
typedef class   arrayOopDesc*               arrayOop;
typedef class     objArrayOopDesc*            objArrayOop;
typedef class     typeArrayOopDesc*           typeArrayOop;

其中oop是Oop体系中的最高父类,整个继承体系如下所示,不同的oop用于表示不同的类,例如instanceOop表示Java中普通的对象,arrayOop则表示数组对象。:

oop(对象)由对象头,对象体(实例数据),对齐填充三部分组成。

 

对象头

对象头中存储了对象很多java内部的信息,如hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间等,Java对象头一般占有2个机器码(机器一次处理的数据位数)。如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块区域用来记录数组长度。 HotSpot虚拟机的对象头包括两部分信息,第一部分为Mark Word,第二部分为class pointer,如果是数组对象,那么还有数组长度。 普通对象和数组对象的对象头结构如下(32位为例):

 

Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word 的位长度为JVM的一个机器码的大小,为了存储更多的信息,JVM将机器码的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

其中各部分的含义如下:

  • lock: 2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
  • biased_lock : lock 状态(2位。和是否偏向锁共同作用,示意如下)
    • 0 01 无锁
    • 1 01 偏向锁
    • 0 00 轻量级锁
    • 0 10 重量级锁
    • 0 11 GC标记
  • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
  • identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • ptr_to_lock_record:指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

64位与32位组成相同,主要是存储位数长度不同:

Klass Pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩。开启该选项后,下列指针将压缩至32位:每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也是一个机器码。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

对象体(实例数据)

对象体存储的是具体的成员属性。值得注意的是,如果成员属性属于普通对象类型,则oop只存储它的地址。每个field在oop中都有一个对应的偏移量(offset),oop通过该偏移量得到该field的地址,再根据地址得到具体数据。因此,Java对象中的field存储的并不是对象本身,而是对象的地址

JVM将Java对象的field存储在oop对象体中,oop提供了一系列的方法来获取和设置field,并且针对每种基础类型都提供了特有的实现。

对齐填充

由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。·

Klass继承体系

与oop类似,klass的继承体系如下:

// hotspot/src/share/vm/oops/oopsHierarchy.hpp
...
class Klass;  // Klass继承体系的最高父类
class   InstanceKlass;  // 表示一个Java普通类,包含了一个类运行时的所有信息
class     InstanceMirrorKlass;  // 表示java.lang.Class
class     InstanceClassLoaderKlass; // 主要用于遍历ClassLoader继承体系
class     InstanceRefKlass;  // 表示java.lang.ref.Reference及其子类
class   ArrayKlass;  // 表示一个Java数组类
class     ObjArrayKlass;  // 普通对象的数组类
class     TypeArrayKlass;  // 基础类型的数组类
...

当然,JVM本身所定义的用于描述Java类的C++类也使用klass去描述,这相当于使用另一种面向对象的机制去描述C++类这种本身便是面向对象的数据。

(1)用于表示Java类。klass包含元数据和方法信息,用来描述 Java 类或者JVM内部自带的C++类型信息。Java 类的继承信息、成员变量 、静态变量 、成员方法 、构造函数等信息都在 klass 中保存 ,一个class文件被JVM加载之后,就会被解析成一个klass对象存储在内存中。JVM据此在运行期可以反射出Java类的全部结构信息。

(2)实现对象的虚分派(virtual dispatch)。所谓的虚分派,是JVM用来实现多态的一种机制。

详见Java的对象模型——Oop-Klass模型

体系总览(待补充)

在JVM内部定义了3种结构去描述一种类型 :oop 、klass 和 handle 类。注意,这 3 种数据结构不仅能够描述外在的 Java 类 ,也能够描述 JVM内在的C++类型对象。Handle是对 oop 的行为的封装(Handle类内部只有一个成员变量一handle,该变量类型是oop*,因此该变量最终指向的就是一个oop的首地址)在访问 Java 类时一定是通过 handle 内部指针得到 oop 实例的,再通过 oop 就能拿到 klass ,如此 handle 最终便能操纵 oop 的行为了(注意,如果是调用JVM内部C++类型所对应的oop的函数 ,则不需要通过 handle 来中转,直接通过 oop 拿到指定的 klass便能实现)。klass 不仅包含自己所固有的行为接口,而且也能够操作 Java 类的函数。由于Java 函数在JVM内部都被表示成虚函数,因此handle模型其实就是 Java  类行为的表达。
三者的关系如下:

Handle体系

handle封装了oop,由于通过oop可以拿到 klass ,而 klass 是对 Java 类数据结构和方法的描述 ,因此 handle 间接封装了 klass。JVM内部使用一个 table 来存储 oop 指针。但是JVM内部采用这种结构对klass进行间接引用是为GC考虑。具体表现在2个地方 :

  1. 通过handle,能够让 GC 知道其内部代码都有哪些地方持有 GC 所管理的对象的引用,这只需要扫描 handle 所对应的 table ,这样 JVM 便无须关注其内部到底哪些地方持有对普通对象的引用。
  2. 在GC过程中如果发生了对象移动(例如从新生代移到了老年代),那么JVM的内部引用无须跟着更改为被移动对象的新地址,JVM 只需要更改 handle table 里对应的指针即可 。

之所以分别给 oop 和 klass 定义了 2 套不同的 handle 体系,是为了方便垃圾回收。本质上,每一个oop,其实都是一个 C++类型,也即 klass;而对于每一个 klass 所对应的 class ,在JVM内部又都会被封装成 oop。在具体描述一个类型时,会使用 oop 去存储这个类型的实例数据,并使用 klass 去存储这个类型的元数据和虚方法表。而当一个类型完成其生命周期后,JVM会触发 GC 去回收,在回收时,既要回收一个类实例所对应的实例数据 oop , 也要回收其所对应的元数据和虚方法表(当然,两者并不是同时回收,一个是堆区的垃圾回收, 一个是永久区的垃圾回收)。为了让 GC 既能回收 oop 也能回收 klass,因此 oop 本身被封装成了 oop ,而 klass 也被封装成 oop。

Java对象模型-oop和klass
 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值