java虚拟机中对象创建过程
我们平常创建一个对象,仅仅只是使用new关键字new一个对象,这样一个对象就被创建了,但是在我们使用new关键字创建对象的时候,在java虚拟机中一个对象是如何从无到有被创建的呢,我们接下来就来谈谈在java虚拟机中一个对象的创建过程(仅指我们平常使用的对象,不包括数组对象和Class对象)。
对象的内存布局
在了解对象的创建过程之前,我们先说说对象的内存布局,在HotSpot虚拟机中,对象在堆中的存储布局可以划分为三部分:对象头(Header)、示例数据(Instance Data)和对齐填充(Padding)。
对象头(Header)
HotSpot虚拟机对象的对象头包括两类信息。
第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另一部分是类型指针,即对象指向它的类型元数据的指针,java虚拟机通过这个指针来确定该对象是哪个类的实例。
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据信息推断出数组的大小。
实例数据(Instance Data)
这部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的自读那都必须记录起来。这部分的存储顺序会 受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)。
如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空 隙之中,以节省出一点点空间。
对齐填充(Padding)
这部分不是必须存在的,也没有特殊的意义,它仅仅起到占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
对象的创建
当Java虚拟机遇到一条字节码new指令时,会首先去检查这个指令的参数是否能正常在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有那必须先执行相应的类加载过程。类加载过程可以看这篇文章。
在类加载完成后对象所需的内存大小便已经确定,接下来就是给新对象在java堆中分配存储空间,这个过程相当于是将一块确定大小的内存块从java堆中划分出来,这个过程有两种方式:
-
指针碰撞
这种方式适用于Java堆中的内存空间是规整的情况,这种情况下被使用的内存在一边,空闲的空间在另一边,中间放着一个指针作为分界点的指示器,这种情况下分配内存就只是将中间的指针向空闲空间的方向移动一段与对象大小相等的距离即可。
-
空闲列标
这种方式适用于Java堆中的内存空间是不规整的情况,这种情况下适用过的内存和空闲的内存交错在一起,这样就不能使用简单的指针碰撞来给对象分配内存了。此时虚拟机维护了一个列表,记录了哪些内存块是可用的,在给对象分配内存时,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录即可。
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值。这步保证了对象的实例字段在Java代码可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到 类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才 计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。
在上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了。但是从Java程序的角度来看,对象的创建才刚刚开始,因为构造函数(即Class文件中的init方法)还没有执行,所有的字段都还是默认的零值。一般来说,new指令之后会接着执行 ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
对象的访问定位
创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具 体对象。对象的访问方式是由虚拟机实现而定的,主流的访问方式有使用句柄和直接指针两种。
句柄访问
如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就 是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
直接指针
如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关 信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问 的开销。
这两种对象访问方式都各有优势,使用句柄访问的最大好处就是reference中存储的时稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不会被修改。
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访 问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。