【Java虚拟机】一个实例对象的生成步骤
本文介绍了一个对象的创建在虚拟机中所经历的过程,并且包含了一个对象在堆内存中的分布和对象的访问方式.
一. 对象的创建过程
对象的创建时在程序中常见的行为.一个实例对象的生成并不只是new这么简单.在程序中声明变量,接收到new的指令后,JVM在方法区中查找相应的类信息,如果还未加载进方法区,则需要进行加载的步骤.当虚拟机确定了对象的类型信息后,就可以为新生对象在堆中分配内存空间了.
1.分配堆中内存
需要内存的大小已经在类加载进方法区后就可知了,在堆中分配对象的方法有两种:指针碰撞和空闲列表.
如果堆中内存是规整的,已使用区域和未使用区域之间维护一个指针来分界,这样的方法叫做指针碰撞.
如果堆中内存不规整,使用和未使用区域交错,虚拟机会维护一个列表来记录哪块内存空间是可用的,这种方法叫做空闲列表.后面提到的标记-清除算法使用的就是该方法来分配空间.
具体使用哪一种方法,内存的规整性取决于采用的垃圾收集器.
2.分配过程中的并发操作
在为新生对象分配过程中,如果线程被挂起,执行上下文切换后空间可能会被其他线程的新生对象占用.为了解决这样的问题,JVM有两种解决方案:
第一种是对分配内存空间的动作进行同步,这样一来这个操作就只能由一个线程进行完成才能释放互斥锁.然而这样会产生新的问题:在运行中产生对象的动作非常频繁,如果使用同步操作会大大降低程序运行的效率.接下来会有第二种常用的方法.
第二种方法是把分配空间的动作按照线程划分,在不同的空间中进行.在每个线程中都预先分配一块内存空间叫做本地线程分配缓冲(Thread Local Allocation Buffer),在这块内存中进行分配是不会产生由并发带来的问题的.而同步也只需要在TLAB这块区域被分配完后才需要进行.
3.对象的初始化
分配完成后,JVM将空间全部初始化为0,这样保证了对象的成员变量在不赋初值也可直接使用.随后JVM为对象进行设置,包括对象头的具体内容.紧接着进行<init>
方法初始化,该方法会给成员变量赋上设置的初值.
至此,一个新生的对象创建完成.
二. 对象的在内存中的分布情况
对象在内存中主要是由三个部分组成的:对象头/实例数据/对齐填充.
对象头中包含的是有关这个对象的一系列信息,HashCode值/锁状态/GC分代年龄等一些信息都存储在这颗对象头中.此外,对象头还包含了告诉JVM"我算什么东西"的元类型指针,这个指针指向的是在方法区中的对象数据类型,也就是class类型,JVM通过访问该指针所对应的数据,知晓这个对象是什么类型的实例.如果对象是一个数组,在对象头中还会存储有关数组长度的相关数据.
实例数据包括了对象中真正存储的有效信息,包括所有从父类中继承下来的数据.并且父类中定义的对象会出现在子类对象之前.
对齐填充并不是必须的.它的作用仅仅在于维持内存空间的一个规范性.JVM要求对象在内存中的大小必须是8的整数倍,如果对象的大小正好为整数倍,那就不需要填充数据;如果不整,那就需要这部分来对该对象进行填充.
以上便是对象的内存分布.
三. 对象的访问定位
方法中的变量通过reference引用的方式来访问到相应的对象,那么虚拟机是如何通过java虚拟机栈中的reference引用找到该对象的实例数据和类型数据的呢?有两种实现的方法:使用句柄和直接指针.
使用句柄的方法在java堆中分配一个区域叫做句柄池.在句柄池中存储的是各个对象在方法区中的类型数据地址和在堆中的实例数据地址.栈帧中局部变量表中的reference中存储的是对象在句柄池中的相应地址.
直接指针的方法则是reference存储的是对象实例在Java堆中的地址,对象类型数据的地址包含在了对象头中.
二者之间各有优势.使用句柄的方法在改变实例地址的时候(可能是垃圾回收后)只需改变句柄池中的引用地址,而无需改变reference的引用.使用直接引用的好处是速度快,它避免了访问句柄池的时间消耗,由于对象的访问在jvm中十分频繁,如此往复可以节省许多时间,提高了效率.HotSpot采用直接指针.
参考:《深入理解Java虚拟机》