前言:java对象是怎么从代码变成一块内存空间的呢?只看代码层面我们只是使用了new关键字加上调用构造器,就生成了一个对象,然后我们就可以使用这个对象了,那么虚拟机在这当中究竟是怎么实现这个过程的呢,在这里我们一起学习下这个过程。
场景假设
1.java代码与字节码信息展示
假设有如下代码,Human是一个类,我们想要创建一个该类的对象供使用,下面我们围绕这段代码展开讨论:
pubic class TestCreateObject{ public void test(){ Human human = new Human(); } }
上方test方法对应的class字节码信息如下图(test方发表信息)
我们可以清晰的看到这一行代码对应的虚拟机指令,这些字节码指令中真正是因为new关键字产生的指令有new指令、invokespecial指令。其他指令dup、aload、astore均于此无关。
2.虚拟机类加载的过程
回忆下类被虚拟机加载的过程,如下图,在说对象创建的时候回频繁使用到这流程,所以先列出来供随时观看。
对象创建过程
1.虚拟机在碰到new指令时,会去检查运行时常量池中是否有Human类的符号引用,根据符号引号去找到这个类的class文件。
2.将Human对应的Class文件加载进入到虚拟机中,这个加载就是上方说话流程图中的加载,同时创建一个class对象。用以反射场景下使用,这里未用到该类的Class对象(java.lang.Class)。
3.当Human的class文件被加载进虚拟机后,虚拟机会对这个class文件的信息进行验证,这是“验证”阶段,比如验证魔数是否是0xCAFEBABE,验证主、次版本号,验证元数据,验证字节码指令、验证符号引用是否真实存在等。
4.当通过了上述的验证后虚拟机会为类变量赋初始值,这是“准备”阶段,这个阶段是专门为类级别的信息赋初始值的,Human中没有类变量则不涉及这个阶段。
5.然后虚拟机就开始去将该类中的符号引用翻译成直接引用,这是“解析”阶段,值得注意的是,解析阶段并不一定发生在这里,也可能是发生在上图中的“初始化”后,这里我们可以就当做在这里发生,以便于理解。将符号引用转化为直接引用后,在使用时就可以直接使用了。
6.到现在为止我们依然没有将该类完全加载完成,这个阶段需要去为类级别的变量赋值,这是“初始化”阶段,Human中没有类级别的变量、或者代码块,所以这块也是省了,注意刚刚说的“准备”阶段是赋初始值,这个阶段是赋程序里面设定的值,并不是一回事。
7.到现在为止Human的类型信息已经完全被加载进了内存中,这里才到了new指令的动作,前面一系列都是因为new指令触发的,并不是new真正要做的事情,虚拟机在堆中开辟一块内存用以存储Human对象,这里对象的分配,有两种方案,如果直接分配在了新生代,那么对象的分配一般使用“指针碰撞”的方式,如果被直接分配在了老年代则有可能使用"指针碰撞"也有可能使用“空闲列表”这主要取决于被分配的区域是否有对空间整理的能力。而且对象分配时虚拟机还会采取安全策略,一般使用CAS+失败重试的方式老保证线程安全,当然也有使用TLAB(本地线程分配缓冲)来保证线程安全的,这是一种为每个线程都预先分配一小块内存的方式。到现在为止一个没有任何数据的对象已经在虚拟机中产生了,一个对象在堆中分为三个部分:对象头、实例数据区、对其补充。
8.虚拟机在分配完内存后,会将除了对象头中以外的信息都进行初始化为0的操作,所以我们在日常开发中,实例变量不进行初始化是可以正常使用的,但是局部变量不实例化却不可以使用。
9.接下来,虚拟机需要为Human的对象设置必要的信息,首先是对象的头部信息,比如GC分代年龄、对象的哈希码等的设置,另外对象的头部中还存储了另外一个比较重要的信息“类型指针”,该指针指向了Human的元数据。
10.在以上所有操作都完成以后,其实一个不包含任何数据的对象就产生了,因为他的实例数据区域都是默认值0,刚刚一开始我们就说过,new关键字在被编译后变成了两条字节码指令,一个是new指令,一个是invokespecial指令,上面这些过程都是new指令完成的,既然new指令的活干完了,肯定就会轮到invokespecial指令了。该指令是虚拟机中5种方法调用指令之一,用以调用构造器、私有方法、父类方法等。这里自然是调用了Human的构造方法了
从上方的标红部分可以看出,该指令的参数就是Human的构造器方法了,该部分运行后就会为对象中的实例数据区域进行赋值了。这样一个完整的对象就在堆中建立了起来,然后虚拟机将Human对象的地址给到占中human这个变量。这就完成了整个过程。