Java虚拟机-对象(HotSpot)
对象的创建
当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有就应先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小,在类加载完成后便可以完全确定。
为对象分配空间的方式总共有两种,指针碰撞(Bump the Pointer)和空闲列表( Free List)。选择哪种分配方式由Java堆是否规整决定,Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理(Compact)功能决定。采用Serial、ParNew等带有压缩整理过程的收集器时,系统采用的分配算法是指针碰撞;而使用CMS这种基于Mark-Sweep算法的收集器时,理论上只能采用复杂的空闲列表来分配内存(CMS为了能在多数的情况下分配的更快,设计了一个叫做Linear Allocation Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区后,里面仍旧可以采用指针碰撞来分配内存)。
分配内存在并发情况下并不是线程安全的。解决这个问题的方法有两种:
CAS: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先分配一块内存,虚拟机在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS为新的TLAB进行内存分配。
指针碰撞:假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。
空闲列表:事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。

初始化对象
内存分配完毕后,虚拟机需要将分配到的内存空间初始化为零值(不包括对象头),如果使用TLAB,初始化工作可以提前到TLAB分配时进行。这一步保证了Java对象的实例在Java代码中不赋初值就能使用。
接下来虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码(实际上对象哈希码会延后到真正调用Object::hashCode()时才会计算),对象的GC分代年龄等信息。此时对于虚拟机来说,一个新的对象实例已经诞生了。
在这之后(new指令完成),会执行构造函数------即Class文件中的<init>方法,把对象按照程序员的意愿进行初始化,此时一个真正可用的对象才算完全产生。

对象的内存布局
对象在内存中可以分为三块区域:对象头(Header)、实例数据(Instance data)和对齐补充(Padding)。
HotSpot VM的对象头包括两部分信息,第一部分用于存储对象自身的运行数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据长度在32位和64位(未开启压缩指针)的虚拟机中分别为32bit和64bit,官方称它为”Mark Word“。对象头的另一部分是类型指针,即对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。另外,如果对象是Java数组,那在对象头中还必须有一块用于记录数组长度的数据。
实例数据部分是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中的顺序影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)。在满足这个条件的前提下,在父类中定义的变量会在子类前面。如果-XX:CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙中以节省空间。
对齐填充并不是必然存在的,也没有特殊含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求任何对象起始地址必须为8字节的整数倍,也就是对象大小必须为8字节整数倍,而对象头正好是8字节的倍数(1/2倍),因此,对象实例部分没有对齐时,就需要通过对齐填充来补全。

对象的访问
对象建立之后就是为了使用,Java程序通过栈上的reference数据来操作对象。reference只是对象的引用,Java虚拟机规范也没有规定应该通过何种方式去定位对象。现在主流的访问方式是使用句柄和直接指针。
如果使用句柄访问的方式,那么Java堆中会划分出一块内存来作为句柄池,reference存储的就是句柄地址,句柄中包含了对象实例数据和类型数据的地址信息。

如果使用直接指针的方式,reference中存放的就是对象实例的地址。

使用句柄的好处是reference中存放的是稳定的句柄地址,在对象被移动后,reference的值无需改变;直接指针的好处是速度更快,它节省了一次指针定位的时间。HotSpot使用的是直接指针的方式。(也有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发)