让我们思考这样一个问题:一个Java对象如何在基于c++实现的系统中运行?对象在JVM内部是如何表示的?它在内存中是如何存储的......
1.OOP-Klass 二分模型
Java是面向对象的语言,面向对象有三个特征:封装、继承和多态。而HotSpot基于C++实现,C++也是面向对象的语言,那这样的话为每一个Java类生成一个C++类不就OK了吗?事实并不是这样,对象在JVM内的表示被设计成了一种新的表示方式:OOP-Klass 二分模型:
■ OOP:或OOPS,即普通对象指针,用来描述对象实例信息。
■ Klass :Java类的C++对等体,用来描述对象实例的具体类型。
为什么要设计这样的模型呢?原因:HotSpot JVM的设计者不想让每一个对象中都含有一个vtable(虚函数表)。
2.OOP-Klass模块:
模块说明如下:
OOP
上面列出的是整个Oops模块的体系结构,其中包含多个子模块,每个子模块对应一个类型,每一个类型的OOP都代表一个在JVM内部使用的特定对象类型。在Java应用程序运行过程中,每创建一个Java类对象,在JVM内部也会相应的创建一个OOP对象来表示Java对象。OOPS类的共同基类型为oopDesc。根据JVM内部使用的对象业务类型,具有多种oopDesc子类,每种类型的OOP都代表一个在JVM内部使用的特定对象类型。
这些OOPS类在JVM内部有着不同的用途:
例如:instanceOopDesc表示类型实例,arrayOopDesc表示数组。也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象;同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDes对象来表示这个数组对象。
在JVM内部,通过instanceOopDesc来表示一个Java对象。对象在内存中的布局可以分为连续的两部分:instanceOopDesc和实例数据。其中,instanceOopDesc或arrayOopDesc又被称为对象头,对象头包含两部分信息:Mark Word(_mark)和元数据指针(_metadata)。其中,Mark Word存储对象运行时记录信息,入哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线性ID等;元数据指针指向描述类型的Klass对象的指针,Klass对象包含了实例对象所属类型的元数据,虚拟机运行时将频繁使用到这个指针定位到位于方法区内的类型信息。
Klass
和OopDesc是其他Oop类型的父类一样,Klass是其他klass类型的父类。如图:
Klass向JVM提供两个功能:
■ 语言层面实现Java类(在Klass基类中已实现)
■ 实现Java对象的分发功能(由Klass的子类提供虚函数实现)
HotSpot JVM的设计者把对象一分为二,分为klass和oop,其中oop的职能主要在于表示对象的实例数据,所以其中不含任何虚函数;而klass为了实现虚函数多态,所以提供了虚函数表。_metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。这两个指针都指向instanceKlass对象,它用来描述对象的具体类型。
JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。
instanceKlass的内部机构:
在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此他们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点,如下图:
在这种设计下,JVM对内存的分配和回收,都可以采用同一的方式来管理。oop一般可以理解为我们常说的对象,klassOop可理解为Java类在JVM中的表示,klassKlass描述了如何表示一个类。为了更形象的表示一个java类在内存中的存储结构,请看下图:
总结:
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceklass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象时,JVM会创建一个instanceOopDesc对象,此对象包含了两部分信息:对象头和元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁信息;元数据维护的是指针,指向的是对象所属的类的instaceKlass。