对象在内存中的整体结构
在堆区中,一个对象的整体结构如下:
对象头 + 实例数据 + 对齐填充
整体结构如图所示:
一、对象头解析
对象头包括内容如下:
运行时数据区(Mark word) + 类型指针(Klass word) + 数组长度记录(非数组对象没有)
在堆区,JVM需要存储大量对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段,用来增强对象的功能,这些标记字段就是在对象头中存储。
以32bit JVM为例,普通对象头的组成如下:
Object Header (64 bits) |
---|
Mark Word (32 bits) + Klass Word (32 bits) |
数组对象的对象头如下
Object Header (96 bits) |
---|
Mark Word (32 bits) + Klass Word (32 bits) + array length(32bits) |
1、运行时数据区 - Mark Word
运行时数据区,一般也称为 " Mark Word ",是一个很重要的内存空间,内部存放了大量对该对象运行时自身的数据标识,比如:gc年龄分代、锁标志位、哈希值等等。
我们知道,对象是可以被锁的,也就是对象锁(也叫方法锁),针对一个对象实例,标识该对象实例是否具有锁,所以只能锁定当前的对象,对其他的对象实例不会产生任何影响。
而对象锁的情况,也是在Mark Word中进行标识的。
目前对象共有如下几种状态:
(1) 正常状态的对象
(2) 加了偏向锁的对象
(3) 加了轻量级锁的对象
(4) 加了重量级锁的对象
(5) 等待GC回收的对象
表1:不同状态的对象所对应的Mark Word结构(在32bit和64bit两种情况下)
Mark Word (32 bit) | status |
---|---|
identity_hashcode:25 bit | age:4 bit | biased_lock:1 bit | lock:2 bit | 正常状态的对象 |
thread:23 bit | epoch:2 bit | age:4 bit | biased_lock:1 bit | lock:2 bit | 加了偏向锁的对象 |
ptr_to_lock_record:30 | lock:2 | 加了轻量级锁的对象 |
ptr_to_heavyweight_monitor:30 | lock:2 | 加了重量级锁的对象 |
lock:2 | 被标示为等待GC的对象 |
Mark Word (64 bit) | status |
---|---|
unused:25 | identity_hashcode:31 | unused:1 | age:4 bit | biased_lock:1 | lock:2 | 正常状态的对象 |
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | 加了偏向锁的对象 |
ptr_to_lock_record:62 | lock:2 | 加了轻量级锁的对象 |
ptr_to_heavyweight_monitor:62 | lock:2 | 加了重量级锁的对象 |
lock:2 | 被标示为等待GC的对象 |
下面逐个分析Mark Word中各个字段的含义:
(1) identity_hashcode:标识对象的hashcode值,只有普通对象中存储。调用System.identityHashCode()生成hashcode后,会写入对象头中,当对象被锁定后,该值会移动到管程Monitor中存储;
(2) age:分代年龄,每经过一次s区的复制,该对象的age增加1;
(3) biased_lock与lock:这两个地方相互配合,共同描述一个对象锁和GC状态,见下图:
biased_lock 标识是否为偏向锁 | lock 锁状态 | 对应状态 |
---|---|---|
0 | 01 | 普通对象 |
1 | 01 | 对象具有偏向锁 |
无此字段 | 00 | 对象具有轻量级锁 |
无此字段 | 10 | 对象具有重量级锁 |
无此字段 | 11 | GC标记 |
可以从上表中看到,lock的2bit和biased_lock的1bit,完成了对对象5种状态的描述,其中biased_lock用来区分了是否为偏向锁。
(5) thread:持有偏向锁的对应线程的ID,只有加了偏向锁的对象具有该标识;
(6) epoch:偏向时间戳;
(7) ptr_to_lock_record:指向栈中锁记录的指针,只有加了轻量级锁的对象内会有该标识;
(8) ptr_to_heavyweight_monitor:指向管程(即对象监视器)Monitor的指针,只有加了重量级锁的对象内会有该标识;
【引用】关于Monitor的资料:http://www.sohu.com/a/328600103_120210224
2、类型指针 - Klass Word
对象头的 " 类型指针 " 部分用于存储对象的类型指针,该指针指向的是该对象所属类的元数据,JVM通过这个指针确定对象是哪个类的实例。
从 “对象整体结构图” 中可以看出来,指向的就是方法区内的类信息。
3、数组长度 - Array Length (只针对数组对象)
就是记录数组长度用的,该数据无论是在32 bit 或者 64 bit中,都是以32 bit大小进行存储数组长度的
二、实例数据解析
实例数据存放的就是在程序中给该对象所属的类定义的各种成员变量,包括从父类继承的。这部分的存储顺序会收到虚拟机分配策略以及成员变量在代码中定义顺序的影响。
三、对齐填充解析
以HotSpot虚拟机为例,对象的大小必须是8字节的整数倍,对象头从上面分析后可以知道,无论32|64 bit ,都是8的整数倍,但是实例数据部分则不一定,对齐填充就是为了补全实例数据部分不是8的整数倍而存在的。
【参考资料】
1、https://www.jianshu.com/p/eaef248b5a2c
2、https://blog.csdn.net/qq_35394891/article/details/82927747
3、https://blog.csdn.net/lkforce/article/details/81128115
Java - 对象模型
先明确一个知识点:
当我们在java程序中new一个对象时,实际上是一条指令 “new” ,当JVM加载.class文件,并执行到new这个指令的时候,从java程序层面来看,是生成了一个java的对象,然而我们从JVM层面上来看,实际上是由两个C++的类的对象共同组成了一个Java对象(JVM就是C++编写的,所以JVM处理java程序其实都要经过C++的转化处理),这个就是Java对象模型:"oop / klass Model " 二分模型
JVM内部实际上定义了各种oop-klass,在JVM看来,不光是java程序中new出来的对象是对象,包括Java的类、方法、字节码中的常量池,这些都是对象。JVM使用不同的oop-klass模型来代表不同的对象。JVM实现后,比如new出来的对象就是instanceoop+instanceKlass、方法在JVM中的对象就是methodoop 、字节码常量池在JVM中的对象就是constmethodoop、Java类的对象就是Klass。
通过下面的研究,可以得出一个结论:oop - klass model,简单来说就是oop是一个普通对象指针,指向klass类实例。【后续会有例子说明】
我们这里再把整体结构如图详细一些:
综合对比之前的图片,可以看出来,其实区别就在于蓝色区域,而Oop / Klass Model 也就是蓝色标注的两个类:instanceOopDesc与instantceKlass
一、浅析instanceOopDesc与instantceKlass
instanceOopDesc:只要new一个类,就会在堆区创建一个该对象;
instanceKlass:只要加载一个类,就会在方法区创建一个该类的对象;
从上面的图中可以看到,instanceOopDesc对象的_mark(对象头)属性中就有指向instanceKlass的属性;
为什么要将java对象拆成两部分C++的对象,而不是采用java对象和C++对象一对一映射的这个方式?
简单来说,在设计时,不希望每个instanceOopDesc对象都包括虚方法,那样太浪费内存空间了,所有虚方法都由Klass对象保存,那么所有A类的实例,都由元数据指针指向Klass,用到虚方法直接method dispatch即可,省空间。
1、instanceOopDesc浅析 - HotSpot中的Oop体系
instanceOopDesc继承于OopDesc,在OpenJdk 1.7 中,Oop层级的继承关系如下图:
Ps:arrayOopDesc是数组对象对应的C++对象;
先看部分oopDesc的代码
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
/*oopsHierarchy.hpp内定义:typedef class markOopDesc* markOop;*/
union _metadata {
wideKlassOop _klass;
/* oopsHierarchy.hpp内定义:typedef class klassOopDesc* wideKlassOop;*/
narrowOop _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
instanceOopDesc继承了OopDesc,故继承了两个数据成员:_mark、_metadata
(1) _mark:是一个markOopDesc类型的对象,用于存储对象自身运行时的数据,也就是上文中说到的对象头中的mark word;
(2) _metadata:是一个struct,_klass是普通指针,_compressed_klass是压缩类指针,都指向一个instanceKlass对象,描述该对象所属的类。_klass这个属性实现了Oop和Klass的关联;
说明一个地方:
在上面的继承图普中,可以发现Oop层级内有一个KlassOopDesc,并且给出的说明是 “描述一个Java类”,那KlassOopDesc和instanceKlass有什么区别呢?
class klassOopDesc : public oopDesc {
public:
// returns the Klass part containing dispatching behavior
Klass* klass_part() const { return (Klass*)((address)this + sizeof(klassOopDesc)); }
// ⭐可以从代码中看出,klassOopDesc中定义了klass_part方法,返回的就是一个Klass对象实例的地址⭐
// 可以参考:http://www.doc88.com/p-4952958367163.html 揭秘Java虚拟机 JVM设计原理与实现 242页
/*.......其余省略 */
2、instanceKlass浅析 - HotSpot中的Klass体系
之前说过,只要加载一个类,JVM会生成一个这个类对应的instanceKlass对象(C++),是虚拟机内部Java类型结构的对等体。
instanceOopDesc继承于OopDesc,在OpenJdk 1.7 中,Klass层级的继承关系如下图:
3、 HotSpot中的Klass体系与Oop体系的关系
从上面的Oop层级图与Klass层级图内,可以发现几个Oop与Klass层级类似的地方:
Oop和Klass基本上都被划分成为:instance、method、constantpool、constantMethod、ethodData、array、klass这几种模型,每个模型都都分别有一个对应的 XXXOopDesc与XXXKlass。
HotSpot认为,通过:数据、方法、类型、数组、实例这几种模型,足以勾画一个完整的Java程序;
举个例子:
A a = new A();
当HotSpot执行到这行时:
1、假设A.class首次加载,那么JVM会先在虚拟机内,创建一个A类的instanceKlass的对象,里面保存了A类的所有信息:变量、方法、父类、接口、构造函数、属性等等,对于虚拟机来说,这个instanceKlass对象就是Java程序中A类的对等体,而instanceOop这个对象可以简单理解为是Java代码中对应的A的 “实例a” ,里面有一个指针指向这个instanceKlass对象