2.3.1 对象的创建
当虚拟机遇到一条字节码new指令时,首先检查能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。
如果没有,那么必须执行类加载过程。
接下来为新生对象分配内存。对象所需内存的大小在类加载完成后便完全确定,把一块确定大小的内存块从Java堆中划分出来,更新”空闲列表“。“空闲列表”记录哪些内存块是空闲的,分配的时候,找到一块够大的内存分配给对象,并更新空闲列表。
如果Java堆是规整的,使用过的放在一边,没使用过的放在另外一边,临界点放一个指针。申请内存后,就移动一下指针,这个时候就不用使用“空闲列表”,使用简答的“指针碰撞”就可以。
选择哪种分配方式,由内存是否规整决定。
对象创建在虚拟机中是非常频繁的,仅仅修改一个指针的位置,在并发情况下也不是线程安全的,两个线程同时修改指针位置的情况。有2中解决方案:第一种是”CAS+失败重试“来保证原子性; 另外一种是本地线程分配缓冲(TLAB),是否使用TLAB,可以用-XX:+/-UseTLAB参数来设定。
内存分配完成后,虚拟机必须将分配的内存空间(不包括对象头)都初始化为0值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时进行。这种操作保证对象的实例字段在Java代码中可以不赋初值就直接使用,试程序能访问到这些字段的数据类型所对应的0值。
接下来,虚拟机还要对对象进行必要的设置,例如,这个对象试哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。
在上面的工作都完成后,从虚拟机角度看,一个新的对象已经产生了。但是从Java程序的角度看,对象的创建才刚刚开始-----构造函数,既Class文件中的<init>()方法还没有执行,所有的字段都是默认0值,对象需要的其它资源和状态信息也还没有按照预定的意图构造好。new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化。
2.3.2 对象的内存布局
在HopSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)、和对齐填充(Padding).
对象头包含两部分:
Mark word: 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
类型指针:即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。如果对象是一个Java数组,那么对象头中还必须有一块用于记录数组长度的数据。
实例数据:对象真正存储的有效信息。无论是从父类继承下来的,还是在子类中定义的字段都必须定义起来。
对齐填充:不是必然存在的,起始地址必须是8字节的整数倍。
2.3.3 对象的访问定位
java程序会通过栈上的reference数据来操作堆上的具体对象。对象的访问方式是由虚拟机实现而定的。主流的访问方式主要有:句柄和直接指针。
句柄:
直接指针: