今天总结的是对象在虚拟机中的创建,布局以及访问。
-
对象的创建
在虚拟机中对象的创建将分为以下几个步骤:1.类的加载检查
首先来看下类的加载检查。每当虚拟机遇到一条new指令时,它将先到常量池中检查是否存在将要创建对象的符号引用,并且检查这个符号引用代表的类是否已经加载,解析和初始化过。如果没有,那必须先加载代表类。(类的详细加载过程将在下篇文章中记录)2.内存分配
类的加载检查通过后,接下来是给对象分配内存。对象所需内存的大小在类加载完成后便能完全确定。简单来说就是在Java堆中找到一块空闲的内存用来存储该对象信息。3.初始化内存空间
对象的内存分配好后接下来就是把这块内存空间都初始化为零。内存空间虽然为零但是对应java代码就是各类型的初始状态。例如int类型的初始化状态为0,布尔类型的初始化状态为false。4.对象必要设置
对象的必要设置包括了该对象是哪个类的实例,如何才能找到类的元数据信息(也就是类信息),对象的哈希吗(用于对象比较),对象的GC分代年龄(用于垃圾回收)等信息。这些信息都将会存储到一个成为对象头的数据结构中。对象头将会在对象布局时说明。 -
对象的布局
在HotSpot虚拟机中(我们介绍的都是基于HostSpot虚拟机),对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
1.对象头:
对象头分为两个部分,第一个部分用于存储对象自身的运行时数据,如哈希吗,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID等。第二部分存储的是该对象的类型指针,也就是这个对象指向的类元数据的指针,虚拟机可以通过这个指针来确定这个对象是属于哪个类的实例的。通过这两个部分我们就能完全掌握对象的实时状态,它属于哪个类,目前是否是锁定状态,是否应该被垃圾回收器回收。简单来说对象头存储的就是关于对象的各种状态。2.实例数据:
对象头我们知道了是专门存储对象状态的一个数据结构,那我们定义的那些字段是存储在哪里的呢?实例数据部分就是真正存储对象有效信息的地方,对应的就是我们java代码中定义的那些各种类型的字段以及字段内容。3.对齐填充:
对齐填充仅仅起到的是占位符的作用。由于虚拟机自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍,如果不是8字节的整数倍就需要通过对其填充来补全。这个还是蛮好理解的,就是给你一个限定容量,如果满足则好,不满足则填满。 -
对象的访问
java程序需要通过栈上的reference数据来操作堆上的具体对象。我们可以理解为程序的入口都是从方法开始的,而方法都是存储在虚拟机栈上的,当我们调用方法时就要到虚拟机栈上创建栈帧,然后开始方法的调用,当需要访问引用对象时这个时候我们就会通过对象的引用到java堆中去找到具体的对象。目前主流的访问方式有使用句柄和直接指针两种。
1.句柄:
如果使用句柄访问方式的话,java堆会像上图一样划分出一块内存来作为句柄池,这种时候java栈本地变量表中的reference中存储的就是对象的句柄地址,而句柄中包含的则是对象的实例数据指针和对象类型数据指针。当我们调用方法需要访问一个引用对象时它的访问流程也就是首先到java栈的本地变量表中找到reference,然后通过reference指向的句柄地址找到对应的句柄,然后找到对应的实例数据和类型数据。
2.直接指针:
相对句柄指针,直接指针就简单的对了,reference中存储的直接是对应于java堆中对象的实例数据,实例数据中又划分了一小块内存存储对象的类型数据指针以便可以在方法区内访问到对象的类型数据。
3.两种访问方式比较:
使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,因为它节省了一次指针定位的时间开销。(HotShpt使用的是第二种方式进行对象访问的)