对象的创建
创建一个对象通常需要new关键字。当虚拟机遇到new指令时,首先检查该指令的参数是否定位在常量池中,并检查该指令所代表的类是否已被加载、分析和初始化。如果执行相应的类加载过程。
类加载检查通过后,虚拟机将为新对象分配内存。将空间分配给对象的任务相当于从Java堆中划分一块确定大小的内存。有两种分布方式。一种叫做指针碰撞。假设Java堆中的内存绝对规则,使用的内存和闲置的内存在一侧,中间有一个指针作为分界点的指示器。分配内存是将指针移动到与对象大小相等的空闲空间的另一侧。另一个叫做空闲列表。如果Java堆中的内存不规则,虚拟机需要维护一个列表来记录哪个内存卡是可用的。在分配时,从列表中找到一个足够大的空间来划分给对象的例子,并更新列表上的记录。采用这种分配方式取决于Java堆是否规则,Java堆是否规则取决于所使用的垃圾收集器是否具有压缩整理功能。另一个需要考虑的问题是对象创建时的线程安全。有两种解决方案:一种是同步处理分配内存空间的动作;另一种是根据线程将内存分配的动作划分为不同的空间,即每个线程在Java堆中预先分配一小块内存(TLAB),在TLAB上分配哪个线程要分配内存,只有当TLAB用完并分配新的TLAB时,才需要同步锁定。
内存分配完成后,虚拟机需要将分配的内存空间初始化为零值。此步骤确保在Java代码中可以直接使用对象的实例字段,而无需赋予初始值。虚拟机需要设置对象,如对象的强度,如果可以找到元数据信息,这些信息存储在对象的对象头中。
上述工作完成后,从虚拟机的角度来看,已经产生了一个新的对象。然而,从Java程序的角度来看,还需要执行init方法,并根据程序元的意愿初始化对象,以便完全生成一个真正可用的对象。
对象的内存布局
在HotSpot虚拟机中,内存中存储对象的布局可分为对象头、实例数据和补充三个部分。
对象头包括两部分:第一部分用于存储对象本身的运行数据,如哈希码、GC分代年龄、线程所持有的锁等。官员称之为“Mark Word"。第二部分是类型指针,即对象指向其类元数据的指针,虚拟机通过该指针确定该对象的类型。
实例数据是对象真实存储的有效信息,也是程序代码中定义的各种类型的字段。
它的填充不一定存在,只起着占位符的作用。HotSpot VM要求对象的起始地址必须是8字节的整数倍,对象的头部只是8字节的倍数,所以当实例数据部分不对齐时,需要填充对齐。
对象的访问定位
Java程序通过堆叠上的reference数据操作堆叠上的特定对象。主要访问方式有两种:使用句柄和直接指针:
句柄:Java堆将将一块内存划分为句柄池,引用中存储的是对象的句柄地址,其中包含对象实例数据和类型数据的具体地址信息。
直接指针:Java堆对象的布局应考虑如何放置访问类型数据的相关信息,并在引用中存储对象地址。
这两种方法都有自己的优点。使用句柄的最大优点是,稳定的句柄地址存储在参考中。当对象移动时,只会更改句柄中实例的地址。参考不需要修改或使用直接指针访问的优点是速度更快,节省了指针定位的时间成本。