head
本节讲解对象的创建过程必须在某种具体的虚拟机上才有意义,笔者使用HotSpot虚拟机为例,HotSpot虚拟机是Sun JDK和Open JDK中所带的虚拟机,也是目前使用最多的虚拟机。
创建对象
以下为普通java对象,不包括数组和Class对象。
1 创建的类是谁
当虚拟机执行 new 指令时会根据指令参数去类常量池中查找类的符号引用,该new命令参数的类型为CONSTANT_Class_info,然后它指向常量池中一个UTF-8的字面量。 检查这个类是否已经加载,链接,初始化。如果没有就先执行类的加载过程。
// 以下是类反编译后常量池和方法 (篇幅有限,有省略)Constant pool: #1 = Methodref #2.#21 // java/lang/Object."":()V #2 = Class #22 // java/lang/Object #3 = Class #23 // io/stephen/Application #4 = Utf8 #21 = NameAndType #4:#5 // " ":()V #22 = Utf8 java/lang/Object public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class java/lang/Object #2为参数指向常量池中的#2,#2为Class的符号引用,它又指向#22的字面量 3: dup 4: invokespecial #1 // Method java/lang/Object." ":()V 7: astore_1 8: return复制代码
2 分配内存
所有的类在加载到虚拟机后便可确定它的大小,这时为这对象在堆中开辟空间即可。这时有两种情况:
1)堆中内存是规整的,既被使用的内存都在一边,未使用的内存在另外一边。两者之间有个指针作为分界点,为对象分配内存时就是把这个指针往未使用的内存挪动一块和该对象大小相同的一块区域,这种方式叫指针碰撞。
2)堆中的内存不规整,已使用的和未使用的内存相互交叉时,虚拟机要维护一个列表,记录那块内存是被使用的,哪块未使用,在分配时从列表中找出一块能容纳下这个对象的内存块分给该对象,然后更新列表的记录,这种方式叫做空闲列表。
可以看出这两种方式的选择由堆内存是否整齐为条件,那堆内存是否整齐又由采用的垃圾收集器是否是压缩整理决定。像Serial、ParNew这种基于压缩算法的垃圾收集器采用的是指针碰撞式。像CMS这种基于标记-清除算法的采用空闲列表式。
另外一个问题是及时保证new对象时候的线程安全性。因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。对此虚拟机也有两种方案:
1)采用了CAS配上失败重试的方式保证更新更新操作的原子性。
2)把内存分配按照线程划分,给每个线程开辟一块内存称为本地线程分配缓存(TLAB),线程在创建对象时就分配在TLAB里,当TLAB用完时才需要同步锁定。可通过-XX:+/-UseTLAB参数设定是否使用此方式。
3 内存初始化
分配结束后,虚拟机要将分配的内存空间都初始化为零值(不包括对象头)。如果使用的TLAB模式,这个过程也可以提前daoTLAB分配时进行。这步的操作保证了对象字段在java代码中可以不赋初始值直接使用。程序能访问到这些字段的数据类型所对应的零值。
4 对象设置
对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。
5 执行init
new指令后会接着执行invokespecial指令,在上面字节码中可以看到该指令,它带有参数#1 可根据其查看要执行的方法为 init。init方法会将类中所有的非静态的字段和代码块收集到它的方法体中进行初始化。如下例:
public class Application { private int n = 2; private BigDecimal b = new BigDecimal(6); { n++; } public Application() { // init() 方法在此执行 System.out.println(n); System.out.println(b); } public static void main(String[] args) { Application a = new Application(); }}复制代码
反编译后的字节码(主要看构造方法):
public io.stephen.dubbo.Application(); Code: stack=4, locals=1, args_size=1 0: aload_0 // 将this入栈 1: invokespecial #1 // Method java/lang/Object."":()V 调用init方法 4: aload_0 5: iconst_2 // 将常量2加载到操作数栈 6: putfield #2 // Field n:I 操作数栈顶元素出栈,并赋值给n 9: aload_0 10: new #3 // class java/math/BigDecimal // 创建bigDecimal对象 13: dup // 将栈顶元素赋值一份压入栈顶 14: bipush 6 // 将常量6压入操作数栈 16: invokespecial #4 // Method java/math/BigDecimal." ":(I)V // 执行BigDecimal的init方法 19: putfield #5 // Field b:Ljava/math/BigDecimal; //弹出栈顶的6赋值给bigDecimal方法参数 22: aload_0 23: dup 24: getfield #2 // Field n:I //访问字段 n 27: iconst_1 // 将常量1压入栈 28: iadd //将栈中头两个元素出栈,并相加,然后把结果从新入栈。 29: putfield #2 // Field n:I // 取栈顶也就是数字3赋值给n // 以下为打印语句,已省略 32: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 35: aload_0 36: getfield #2 // Field n:I 39: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 52: return // 结束方法执行将操作数栈顶的元素返回给调用者复制代码
现在这个对象创建完毕。终于可以歇歇了。