原文链接:JVM系列:(九)对象的创建和访问
一 对象的创建
Java语言是一门面向对象语言,在程序运行过程中无时无刻都有新的对象被创建出来。在Java程序中,创建对象的方式有多种,除了最常用的new关键字外,我们还可以通过反射机制、Object.clone 方法、反序列化以及 Unsafe.allocateInstance方法来新建对象。
其中,Object.clone方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。Unsafe.allocateInstance方法则没有初始化实例对象字段。而new语句和反射机制则是通过调用构造器来初始化实例字段。
我们通过最常用的new关键字新建对象时,虚拟机新建对象时需要经历下面几个步骤:
类加载过程1.1 类加载检查
当JVM检测到一条new指令时,首先先检查该参数是否在常量池中定位到一个类的符号引用,并检查这个类的符号引用代表的类是否已被加载、解析和初始化。如果存在的话,JVM将直接使用已有的信息对该类进行操作。
如果没有,则执行相应的类加载过程。
1.2 分配内存
类加载检查通过后,虚拟机为新生对象分配内存,对象所需内存大小在类加载完成后就可以完全确定,为对象分配空间就是从Java堆中划分一块大小确定的内存。
不同的JVM垃圾收集器在分配内存时表现也不相同,具体表现有两种:
- 如果垃圾收集器选择的是基于压缩整理算法的,那么内存是规整的。所有用过的内存在一边,没有使用的在另一边,中间放着一个指针作为分界点的指示器。
- 如果垃圾收集器选择的是基于标记-清除算法的,那么内存不是规整的。已使用的内存和未使用的内存相互交错,虚拟机维护一个列表,记录哪些内存是可用的以及内存块的位置和大小。
1.3 必要的设置
内存分配结束后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头),这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
其次是对对象进行必要的设置,例如对象是哪些类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等(这个后面垃圾收集时再细说)信息。这些信息存放在对象的对象头之中。
1.4 初始化对象
当完成上述操作后,对象的内存分配成功了,但所有的字段都还是零值。此时会执行<init>方法,把对象按照代码所写的那样进行初始化,从而产生一个真正可用的对象。
二 对象的内存布局
对象的内存布局可以分为三个区域:对象头、实例数据、对齐填充。
- 对象头:非固定的数据结构。一来是用来存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。二来是类型指针(对象指向它的元数据的指针),JVM通过这个指针来确定该对象是哪个类的实例对象,如果对象是一个Java数组,对象头中还需要有一块用来记录数组长度的数据。
- 实例数据:存储对象真正有效的数据,也就是程序代码中定义的各种类型的字段内容。不论是从父类继承的,还是子类定义的。这部分的存储顺序会受到Java源码中的定义顺序的影响。
- 对齐填充:不一定必须存在,起到占位符的作用。因为JVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。故当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
三 对象的访问方式
对象的访问在Java中无处不在,是最普通的程序行为,但即使是最简单的访问,也会涉及Java栈、Java堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:
Object obj = new Object();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。而“new Object()”这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的构造化内存。另外,在Java堆中还需要包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
3.1 使用句柄
使用句柄的访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示:
通过句柄访问对象3.2 直接指针
如果使用直接指针访问方式,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址,如下图所示:
通过直接指针访问对象这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,对于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
我们使用最多的主流虚拟机Sun HotSpot就是使用的直接指针的方式进行对象访问的。
四 总结
本章主要讲解了对象的创建过程、在堆内存中的存储结构和访问方式,通过对象的访问我们可以把前面讲解的JVM内存模型中的方法区、堆、栈这三者关联起来,之间相互配合完成对象的创建及访问。
扫码关注有惊喜
网友评论