对象的创建
在语言层面上,创建对象(例如克隆、反序列化)通常只是一个new关键字而已,而在虚拟机中,是怎么创建的?
虚拟机遇到一条new指令时
1、首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,就执行相应的类加载过程。
2、在类加载检查通过后,虚拟机为新生对象分配内存,对象所需内存的大小在类加载完成后便可以完全确定,等同于把一块确定大小的内存从Java堆中划分出来。如果Java内存规整,只需移动已用/未用空间分界点的指针,叫做“指针碰撞(Bump the Pointer)”,如果不规整,虚拟机必须维护一个列表,记录哪些内存块可用,叫做“空闲列表(Free List)”,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
3、内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零(不包括对象头),如果使用TLAB(本地线程分配缓冲,Thread Local Allocation Buffer,Java堆为每一个线程预先分配的一小块内存),这一工作过程也可以提前至TLAB分配时进行。这一步骤保证了对象的实例字段在Java代码中可以不被赋初值就能直接使用。
4、上面的工作完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序的视角来看,对象创建才刚刚开始——<init>
方法还没有执行,所有的字段都还是零。因此执行new指令后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
HotSpot虚拟机的对象头包括两部分,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳(13章锁优化的内容)等。第二部分是类型指针,即对象指向它的类元素的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,对象头还有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组大小。
实例数据
存储真正的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。相同宽度的字段总是被分配到一起,在满足这个条件的前提下,在父类中定义的变量会出现在子类之前。
对齐填充
不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。由于对象头部分正好是8的整数倍(1或2倍),如果实例数据部分没有对齐,就需要通过对齐填充来补全。