一个对象的实例中到底有什么信息?

一个对象的实例中到底有什么信息?

 

记得上回我在说堆内存的时候,说到,堆内存中存放的都是对象的实例数据,一个个的实例。

然后再后来的对象实例的创建中,我们也提到了对象实例的创建过程,也提到了对象实例。

 

那么问题来了,对象实例到底有哪些数据组成,为什么这么多实例对象呢?我们带着这个问题去分析一下对象的实例中的内存布局情况。

 

 

1.话不多说,先上一张模型图。

大家可以先看看,我们带着这张图进行分析,可以现在看不懂,但是在我们分析后,就需要看懂了。

 

 

2.图片上很明显的两个内存区域,我们也很熟悉,分别是堆和方法区。看图片的意思是这两个内存区域建立起联系了,是的在我们的运行时数据区中,很多内存区域都是相互访问的,而不是相互隔离的。

 

3.那就开始分析一下一个Java实例对象的组成情况吧

图中有一个Java对象实例,我们以一个Java对象实例为例,分析一下其中的结构内容

 

上图是一个Java实例对象的内存结构,我们可以看到其中分为了3部分;分别是:对象头、对象体、其他;

对于这个种结构划分,我们在web项目中的request也见到过类似的结构划分。可以看到图片中也进行了简单的文字描述,但是不够直观,我们接着再细聊一下。

 

4.对象头

(1)在对象头中存在一个区域“标记字(Mark Word)

在不同的位数的操作系统上会有不同的容量长度,例如:32位的虚拟机技术32个bit,64位就是64个bit。

那么标记字中存放的内容是什么呢?

主要就是当前对象的线程锁的状态,以及该对象的hashCode,还有一些用于GC操作的数据。

 

并且在不同的对象的状态下,标记字所存放的内容也是不一样的。

 

这里贴一下《深入理解Java虚拟机》的一张图。

这张图怎么理解呢?

我们可以看到右边有5种状态(未锁定、轻量级锁定、膨胀(重量级锁定)、GC标记、可偏向);

这5种状态,我们如果有学习过synchroized的锁升级的过程就很眼熟了,分别是未锁定->偏向锁->轻量级锁->重量级锁;其还剩一种,就是GC过程中的标记对象进行回收的概念了。

了解锁升级的过程,就是线程最后升级成阻塞挂起的一个操作,影响到内核态的切换问题,消耗资源了。

此处,我们需要再复习一下锁升级的过程,然后结合Java的实例对象的对象头中的数据变化情况进行分析分析了。

a.在一个实例对象刚创建的时候,这个时候,这个实例处于无锁定的状态,那么其标记字的数据内容主要就是 对象的哈希值和对象的分代年龄了。并且其中的偏向锁标识值为0,锁的状态就是01;如下图所示。图片很清晰的结构,其中0标识没有偏向锁;锁状态01未上锁;然后age表示当前的对象的GC次数(年龄达到就得进入老年代或者永久代了);hashCode就不说了,调用的时候就会生成。

 

b.那么实例对象被升级为偏向锁后,又是一个什么结构呢?

其中相较“未上锁”状态而言,肯定多了“锁偏向的线程的ID”,否则怎么记录偏向线程?然后就是偏向的时间戳;还有GC的年龄周期;再择就是当前对象实例的偏向锁的标识变为1了,然后锁的状态还是01。偏向线程的ID数据,可以在线程请求锁的时候,可以直接进入锁代码块,效率高效。

 

c.当2个线程进行锁竞争的时候,锁将会再次升级,升级为轻量级锁。

其内部竞争的方式目前采用的是自旋方式,2个线程不断进行抢占锁的持有权。

那么这个时候对象头中的MarkWord中的组成就会很大的变化了。不在有偏向的线程ID,偏向时间戳,当前对象的GC年龄也没有了,偏向锁的概念也没有了,升级为另一种锁的状态00;

然后就会记录当前对象被锁的栈帧中的锁的地址

 

d.当2->n个线程进行锁的竞争的时候,那么将不能采用自旋的方式了,这会极大消耗CPU的性能,此刻锁再度升级,升级为重量级锁。因为线程过多,都在抢占锁的持有权,那么这个时候,对象头中Mark Word将不是记录指向栈帧中的地址值了。而是直接执行锁的监视器的地址,那么监视器中会管理所有线程,这样也就可以建立起所有线程的关联。然后锁的状态又会变为10;

 

e.上述我们一直有一个对象的年龄的记录,是用于配合GC进行操作的,那么也会存在一个当前对象被标记GC了的状态,那么这个即将被垃圾回收的对象,对象头中的Mark Word中存放的数据会有什么呢?应该是空的了。这么理解,当一个合同已经到期的门面,老板肯定不会再做生意了,应该就是提前在门口声明说合同到期了。然后里面的东西都搬空了。同理,一个被GC标记过的对象实例,也是如此。

 

这就是在对象处于不同的状态下对象头中的标记字内容会有不同的存储内容。贴一下汇总图片

 

 

(2)说完了对象头一定会存在的标记字,那么在说一个可能一定会存在的“类型指针(Klass Word)”数据,也属于对象头中的一部分。

之前在我们分析对象实例的构建的步骤中,提到过很关键的一步,就是去方法区中的常量池中寻找实例化该对象的类元数据信息,然后会将其类元数据地址和我们堆内存中的实例建立关系,进行绑定。此刻,我们所讲的“类型指针(Klass Word)”就是建立绑定关系的数据记录地方。

我们之前举例子,一个月饼模具可以制作很多月饼,那么这个月饼自然需要知道他是被哪个模具制作出来的,这个模具是由什么材质制成?什么时候制成的?这些元数据信息,都是对实例进行公开的。

关于类型指针中有一个关键性的概念需要学习,就是不同的位数虚拟机,将会有不同的初始大小。

我们上面分析标记字的大小的时候,默认都是一个标记字64bit,那么此处的类型指针也是同理。

但是当我们使用64bit的类型指针和32位的空间消耗,相较而言,64bit肯定多。那么jvm就需要做一些优化处理了。这里将采用一种指针压缩的技术。

我们上面说到,”类型指针“不一定会存在,是的。我们一个堆内存中的实例对象,想获取源类元数据,不一定需要通过对象头中的类型指针去绑定,其他方式也可以获取。

 

(3)在后面,还有一个特殊的对象头,就是数组长度

顾名思义,就是指当前对象实例是一个数组的时候,那么这个对象的对象头中就需要记录这个数组对象的长度。

至于为什么需要记录数组的长度放在对象头中呢?因为要想确定一个Java对象的大小,需要通过元数据进行确认,但是数组中存放的是多个元数据,那么就需要确认整个对象的大小,这样便于内存空间分配。

此处也受虚拟机位数的限制,不同位数会有不同的bit空间,也会受到指针压缩的开关控制。

 

5.对象体

这个就很简单理解了,对象对象,就是得有对象的主体信息。

我们所意识里的对象的主体信息就是我们所关注的对象信息。

例如一个User对象,里面有用户ID,用户名称,用户年龄,用户身高,用户体重等等等,这些都是用户对象的信息,那么这部分数据信息,都是存放在对象体中的。然后用户对象还可能存在继承关系,假设又有一个Admin对象,这个对象继承User类,那么这个Admin中的对象体数据还需要存储User类中的一些属性,然后再加上Admin的属性。

其中对象体中的属性信息,也会存在一个顺序问题,通常情况,虚拟机默认的分配顺序为

longs、doubles、ints、shorts、chars、bytes、booleans、oop(Ordinary Object Pointers);

父类属性在子类前。

 

6.对齐填充

这个命名是真的比较高级哈,也不知道jvm虚拟机规范中是谁将其翻译过来的。

不过确实很好理解了,对其填充,我们都知道一个对象很有可能都是长度长短不一,各不相同的。那么对于堆内存中的分配问题,怎么弄呢?

假设每个对象实例,默认给分配8字节;然后有一个对象实例对象头+对象体消耗了6个字节;另一个对象实例对象头+对象体消耗了7个字节;

这样弄,那么第一个实例多出来的2个字节和第二个实例多出的1个字节怎么弄呢?这不是坑坑洼洼的那么,我们都知道,在物理位置上,我们想尽可能的保证数据的连续性,那么数据的长度不一,就会出现很多的碎片区域,这样就会影响内存的读写管理。

这种时候,就引申出了这种对齐填充的区域,用于将所有对象出现过空闲的碎片区域进行填充,填满,就好比超市卖东西,我们收银的时候,会碰到有1.25元的物品,但是我们没有5分的钱币,那么顾客这个时候可能会买4个一样的物品,这样就消费了5元,这样单位就好计算了。

在HotSpot虚拟机中也是同样设定,在内存管理系统中,对象的起始地址位置必须是8字节为整倍数。这样便于我们拿取分配。在上图中,对象头已经是64bit/32bit了,已经归整了,通常这个对齐填充是为了配合对象体中的数据不规范问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值