Java作为一门面向对象语言,在运行过程中无时无刻都伴随着对象的创建。在语言层面,我们常常是用new来进行一个对象的创建,而其在Java虚拟机中的细节却要复杂许多,创建之后的内存布局和访问方式也值得探讨,下面将以HotSpot虚拟机为例进行一一介绍。
JVM中对象的创建
1、类加载过程
当虚拟机遇到一条字节码的new指令时,首先它会先检查是否能在常量池中定位到一个类的符号引用,并检查这个符号引用对应的类是否已经被加载,若未加载,会先进行类的加载过程。
2、内存分配
在类加载检查之后,JVM会在堆中为当前对象分配内存(大小在类加载之后就可确定)。根据堆中内存是否规整(受垃圾收集器影响)可分为两种方式:指针碰撞(Bump the Pointer)和空闲列表(Free List)。
当堆中内存是规整的,即空闲内存在堆中一边,使用过的内存在另一边,我们使用指针碰撞,内存的中界有一个指针指向,我们只需要把指针往空闲部分移动一个对象内存大小的距离即可,这种方法较为简单高效。
相反,当内使用过的内存和空闲内存交错在一起,我们使用空闲列表来解决,JVM维护一个列表记录哪些内存是可用的,当要分配对象时我们在列表中查找一块合适的内存进行分配,之后将列表进行更新即可。
3、初始化
在这一步我们要将创建后的对象的内存空间初始化为零值,保证了对象的实例字段在不赋初始值就可直接访问。
4、对象头设置
JVM需要对对象进行必要设置,例如声明该对象是哪个类的实例、如何找到类的元数据信息,对象的GC分代年龄等信息,这些信息存放在对象头中。
5、执行<init>( )方法
一个对象的创建往往是用构造函数来进行预定意图的构造,这就对应着Class中<init>()方法,在上面步骤进行之后会执行<init>()方法来对对象进行初始化以满足程序员的需求,这样子一个真正可用的对象就创建完成。
对象的内存布局
对象在堆内存中的布局可划分为三个部分:对象头、实例数据和对齐填充。
1、对象头
对象头主要存储两类信息。一是对象自身的运行时数据,如哈希码,该对象的GC分代年龄、锁状态标识,线程持有的锁等。二是类型指针,即对象指向他的类型元数据的指针,JVM通过这个指针来确定其所属的类。若对象是一个数组,对象头还应该有一块用来记录数据长度。
2、实例数据
实例数据存储的就是真正的对象有效信息,即程序代码中所定义的各个类型的字段信息,无论是父类还是当前类的信息都应该存储。
3、对齐填充
这部分并没有实际意义,因为hotspot JVM要求对象起始地址必须是8字节,这部分起到占位符的作用。
对象的访问
创建一个对象之后我们会在之后进行引用,Java程序会通过虚拟机栈中的reference进行对象引用,而reference主要通过两种方式进行对象访问。
第一种情况是句柄访问,reference存储的是句柄地址,句柄中包含了对象实例数据和类型数据的各自地址。
第二种是直接访问,reference中存储的是对象地址。
具体如图所示。
两种方式各有优点,当我们使用句柄时,即使对象被移动,我们的reference不用被修改,只要修改句柄即可。而直接访问则速度更快,节省了一次地址读取的时间。
以上即是对象在JVM中创建、内存布局和访问的细节,是我在读《深入理解Java虚拟机》之后的理解总结,之后将会随着学习进度进行更新。