Java跟c++语言一样,都是面向对象的语言,那么面向对象的语言都有个共同的两点
- 在面向对象的软件中,对象(Object)是某个类(Class)的实例。
- 一切皆对象
在JVM的内存结构中,对象保存在堆内存中,而我们在对对象进行操作时,其实操作的是对象的引用。
那么对象本身在JVM中的结构是什么样的呢?这个就需要基于HotSpot虚拟机来研究了
简单的介绍一下HotSpot虚拟机
HotSpot虚拟机是基于c++来实现的,他是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的虚拟机 。他的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。 通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
在HotSpot虚拟机中,有个叫oop-klass model的设计模型
Java的对象模型定义在oops目录下,模型主要定义了Java语言相关的模型结构,是Java面向对象特征在虚拟机中的映射。在模型树中主要由klass,oop,metadata,handle来组织(klass继承自metadata),这里我们主要讲klass,和oop两个模型
在oopsHierarchy.hpp里定义了oop和klass各自的体系
oop-klass结构
oop模型
Java对象在虚拟机中的表示
hotspot/src/share/vm/oops/oopsHierarchy.hpp
//java对象中oop的偏移量而
typedef juint narrowOop;
// 如果压缩klass指针,则使用窄klass
typedef juint narrowKlass;
typedef void* OopOrNarrowOopStar;
//对象头标记
typedef class markOopDesc* markOop;
#ifndef CHECK_UNHANDLED_OOPS
//对象头(包含markOop),包含Kclass
typedef class oopDesc* oop;
//Java类实例对象(包含oop)
typedef class instanceOopDesc* instanceOop;
//Java数组(包含oop)
typedef class arrayOopDesc* arrayOop;
//Java对象数组(包含arrayOop)
typedef class objArrayOopDesc* objArrayOop;
//Java基本类型数组(包含arrayOop)
typedef class typeArrayOopDesc* typeArrayOop;
#else
上面列出的是整个oops模块的组成结构,其中包含多个子模块。每个子模块对应一个类型,每一个类型的OOP都代表一个在JVM内部使用的特定对象的类型。
从上面的代码中可以看到,有一个变量oop的类型是oopDesc,oops类的共同基类型为oopDesc。
在Java程序运行过程中,每个创建一个新的对象,在JVM内部就会相应的创建一个对应类型的oop对象,也可以理解为hotspot直接copy一份java的东西给了c++。在HotSpot中,根据JVM内部使用的对象业务类型,具有多种oopDesc的子类。除了oopDesc类型外,oop体系中还有很多instanceOopDesc、arrayoopDesc等类型的实例,他们都是oopDesc的子类。
这些oops在JVM内部中有着不同的用途,例如,instanceOopDesc表示类实例,arrayOopDesc表示数组。也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象。同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDesc对象来表示这个数组对象。
在HotSpot中,oopDesc类定义在oop。hpp中,instanceOopDesc定义在instanceOop.hpp中,arrayoopDesc定义在arrayOop.hpp中。
简单的看一下相关的定义
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
private:
// field addresses in oop
void* field_base(int offset) const;
jbyte* byte_field_addr(int offset) const;
jchar* char_field_addr(int offset) const;
jboolean* bool_field_addr(int offset) const;
jint* int_field_addr(int offset) const;
jshort* short_field_addr(int offset) const;
jlong* long_field_addr(int offset) const;
jfloat* float_field_addr(int offset) const;
jdouble* double_field_addr(int offset) const;
address* address_field_addr(int offset) const;
}
class instanceOopDesc : public oopDesc {
}
class arrayOopDesc : public oopDesc {
}
从上面的源码可以看出来,instanceOopDesc实际上就是继承了oopDesc,并没有增加其他的数据结构,也就是说instanceOopDesc中主要包含了一下几部分数据:markOop _mark和union _metadata以及一下不同类型的field。
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。在虚拟机内部,一个Java对象对应一个instanceOopDesc的对象。其中对象头包含了两部分内容: _mark和 _metadata,而实例数据则保存在oopDesc中定义的各种field中
_mark
对象头中有和锁相关的运行时数据,这些运行时数据是synchronized以及其他类型的锁实现的重要基础,而关于锁标记,GC分代等信息均保存在这里。
_metadata
这是一个共同体,其中_klass是普通指针,_compressed_klass是压缩类指针。在深入介绍之前,就要来到oop-Klass中另外一个主角klass了
klass模型
Java类在虚拟机中的表示
hotspot/src/share/vm/oops/oopsHierarchy.hpp
// klass层次结构与oop层次结构是分开的。
//继承自 Metadata, 维护着类继承结构的类信息,对应一个ClassLoaderData
class Klass;
//继承自Klass,维护着对应的Java类相关信息(注解,字段,接口,虚表,版本,线程等信息),以及类所处的状 态
class InstanceKlass;
//继承自InstanceKlass,用于java.lang.Class实例,与反射相关,除了类的普通字段之外,它们还包含类的静态字段
class InstanceMirrorKlass;
//继承自InstanceKlass,它是为了遍历这个类装入器指向的类装入器的依赖关系。
class InstanceClassLoaderKlass;
//继承自InstanceKlass,与java/lang/ref/Reference相关
class InstanceRefKlass;
//继承自Klass,数组相关
class ArrayKlass;
//继承自ArrayKlass,对象数组
class ObjArrayKlass;
//继承自ArrayKlass,基本类型数组
class TypeArrayKlass;
和oopDesc是其他oop类型的父类一样,klass类是其他klass类型的父亲。
Klass向JVM提供了两个功能
- 实现语言层面的Java类(在Klass基类中已经实现)
- 实现Java对象的分发功能(由Klass的子类提供虚拟数实现)
在hotsprot的设计之初,HotSpot JVM的设计者不想让每个对象中都含有一个虚函数表。
于是把对象一拆为二,分为klass和oop。其中oop的主要职能在于表示对象的实例数据,所以其中不含有任何虚函数。而klass为了实现虚函数的多态,所以提供了虚函数表。所以,关于Java的多态,其实也有虚函数的影子在。
_metadata是一个共同体,其中_klass是普通指针,_compressed_klass是压缩类指针。这两个指针都指向instanceKlass对象,他用来描述对象的具体类型。
instanceKlass
JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。
instanceKlass的内部结构
//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;
可以看到,一个类该具有的东西,这里面基本上都包含了。
还有一点需要简单的介绍一下
在JVM中,对象在内存中的基本存在的形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此他们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,他就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点。关于对象和数组的klass链大致如下图:
在这种设计下,JVM对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klassKlass关系如图:
内存存储
关于一个Java对象,他的存储是怎样的,一般很多人都会回答:对象存储在堆上。稍微好一点的人会回答:对象存储在堆上,对象的引用存储在栈上。我们再来一个更牛逼的回答
对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。
其实如果细追究的话,上面这句话优点故意卖弄的意思,因为我们都知道,方法区用于存储虚拟机加载的类信息、常量、静态变量、即使编译器后的代码等数据。所谓加载的类信息,其实不就是给每个人被加载的类都创建一个instantKlass对象。
可以参考下面这段代码
class Model
{
public static int a = 1;
public int b;
public Model(int b) {
this.b = b;
}
}
public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);
}
存储结构如下:
从上图可以看到,在方法区的instantKlass中有一个int a=1的数据存储。在堆内存中的两个对象的oop中,分别维护着int b=3.int b=2的实例数据。和oopDesc一样,instantKlass也维护着一些fields,用来保存类中定义的类数据,比如int a=1。
总结
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,对象头以及元数据。对象头中有一些运行时的数据,其中就包括和多线程相关的锁的信息。元数据其实就是维护的是指针,指向的是对象所属的类的instanceKlass。
参考资料: