HotSpot的Java对象模型之Oop-Klass模型详解
作者:碳基生物与硅
Java对象模型
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用来实现多态的一种机制。
体系总览(待补充)
在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个地方 :
通过handle,能够让 GC 知道其内部代码都有哪些地方持有 GC 所管理的对象的引用,这只需要扫描 handle 所对应的 table ,这样 JVM 便无须关注其内部到底哪些地方持有对普通对象的引用。
在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。
到此这篇关于HotSpot的Java对象模型之Oop-Klass模型详解的文章就介绍到这了,更多相关HotSpot的Oop-Klass模型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!