我们都知道在java要使用一个对象,通常都是通过关键字new出来一个。但是如果我要问你这个对象是怎么创建,对象在内存中如何布局,对象的大小又怎么计算等等。这一系列的问题如何还不是很清楚,那么今天我们就一起来探究这些问题。

本文讨论的对象限于普通 Java 对象,不包括数组和 Class 对象等

普通对象的创建过程

怎样通俗的理解java的对象(你真的了解Java的对象吗)(1)

对象创建过程

类加载检查

当 Java 虚拟机遇到一条字节码指令 new 时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。此过程在我之前的文章《Java类加载器》已经讲解过。

分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。分配内存的方式有“指针碰撞”和“空闲列表”两种,选择哪种方式由java堆是否规整决定,java堆是否规整又由所采用的的垃圾收集器是否带有压缩整理功能决定。

适用堆内存规整的情况,即没有内存碎片。

原理:用过的内存全部整合到一边,没用过得内存放在另一边,中间有一个分解值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。

GC垃圾收集器:Serial,ParNew

适合堆内存不规整的情况下。

原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。

GC收集器:CMS

此处涉及到的GC收集器在后续的文章中会进行进一步介绍。

初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一操作保证了对象的实例字段在java代码中可以不赋予初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,这些信息存放在对象头,此外,是否开启偏向锁,锁的标志位都在对象头中设置。

执行init方法

经过以上步骤后,从虚拟机的视角看,一个新的对象已经产生了,但从java程序的视角来看,对象创建才刚开始,<init>方法还没有执行,所有字段都还为零,所以一般来说,执行完new指令会执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象在内存中的布局

怎样通俗的理解java的对象(你真的了解Java的对象吗)(2)

对象在内存中的布局

对象创建完成后在内存中保存的信息包括对象头实例数据对齐填充三类信息。

对象头

对象头的数据总共包含了 3 个部分,以下是各个部分的用途:

包含一系列的标识,例如锁的标记、对象年龄等。在32位系统占4字节,在64位系统中占8字节

指向对象所属的 Class 在方法区的内存指针,通常在32位系统占4字节,在64位系统中占8字节,64位 JVM 在 1.6 版本后默认开启了压缩指针,那就占用4个字节

如果对象是数组,还需要一个保存数组长度的空间,占 4 个字节

实例数据

实例数据里面主要是对象的字段数据信息。

对齐填充

对齐填充数据不是必须的,另外填充数据可能在实例数据末尾,也可能穿插在实例数据各个属性之间。JVM 堆中所有对象分配的内存字节总数必须是 8N,如果对象头和实例数据占用的总大小不满足要求,则需要通过对齐数据来填满。

对象大小

如果给一个new Object(),它的大小应该是多少呢?我们来分析一下,它的对象头中的mark word占用8个字节,Class Pointer在开启指针压缩的情况下占用4字节。实例没有属性所以占用是0,此时共占12个字节,因为需要对齐填充所以就是16个字节。

我们也可以通过用程序的方式来计算一下这个对象大小,因为java对象没有相应的api来直接计算对象大小的方法,所以我们采用java中agent的方式来计算。具体如下:

程序编写

怎样通俗的理解java的对象(你真的了解Java的对象吗)(3)

ObjectSizeAgent

此处的利用Instrumentation对象的getObjectSize方法可以轻松获取对象大小,此处这个对象的获取我们可以使用agent的方式,在代理的过程中得到会将此对象传入到premain的第二个参数中。另外需要注意的是我们还需要在当前工程的src下编写MANIFEST.MF,具体内容如下:

怎样通俗的理解java的对象(你真的了解Java的对象吗)(4)

MANIFEST.MF

测试

编写将当前工程达成jar放入到我们测试工程的依赖包下,现在进行代码的测试,在测试时还需要注意我们需要添加虚拟机启动参数使agent失效,具体如下:

怎样通俗的理解java的对象(你真的了解Java的对象吗)(5)

测试对象大小的类

此工程需要依赖前一步生成的jar包,并且启动时通过-javaagent:添加jar地址。

通过执行观察输出结果:

怎样通俗的理解java的对象(你真的了解Java的对象吗)(6)

测试结果

测试结果分析

通过观察我们发现和我们之前判断的objec大小是一样的。

此处需要注意的两个虚拟机启动参数

怎样通俗的理解java的对象(你真的了解Java的对象吗)(7)

测试环境虚拟机默认启动参数

此参数的作用就是用于压缩对象头中Class Pointer指针,压缩后的大小就是4否则是8。

我们可以通过取消此参数进行验证。虚拟机启动时添加-XX:-UseCompressedClassPointers

怎样通俗的理解java的对象(你真的了解Java的对象吗)(8)

测试结果

我们可以发现数组a的大小变为了24,因为对象头中的Class Pointer变为8后,是20个字节,经过对齐就是最终的24字节了。

此参数的作用就是压缩对象实例中对应引用字段的大小,和前一个参数类似,开启后未4个字节,否则是8个字节。此处不再演示。

其实java对象头中具体存储的内容还很复杂,包括锁,GC等信息。大家可以进一步查阅资料来学习。

,