本节内容主要介绍JVM内部对象的特性和生命周期,包括对象的创建过程、对象的内存结构属性和对象的访问定位。掌握了本节内容,将会对分析对象的生命周期及代码的运行流程有很大的帮助。
对象的初始化是在创建对象时由JVM完成的。本节主要介绍创建对象的方式及创建对象的整体流程,其中常用方式有以下6种。
(1)使用new关键字。
(2)Class对象的newInstance()方法。
(3)构造函数对象的newInstance()方法。
(4)对象反序列化。
(5)Object对象的clone()方法。
(6)使用Unsafe类创建对象。
以上6种方式中,最直观的一种就是使用new关键字调用一个类的构造函数,显式地创建对象,这种方式称为由执行类实例创建表达式而引起的对象创建,而这个创建方法也被我们称为由程序执行时的类对象实例创建表达式所自动产生的对象创建。
如果该类是第一次创建对象,那么使用new关键字创建对象时可分为两种:加载并初始化类和实例化对象。在JVM的开发实施过程中,对象在可以被实际应用前必须先进行初始化,而这一点也是Java体系规范所规定的。
类加载阶段:Java类加载系统是通过双亲委托模式实现对类的加载的。
初始化阶段:在实例化对象前会先检查与对象相关的类是否已加载并初始化在内存中,如果不是,会先使用类的全限定名加载类;然后进行链接阶段的操作;最后在初始化阶段调用类构造器(<clinit>)给静态变量赋值,并运行静态代码块。
实例化创建阶段:在执行类的实例化或初始化过程结束后,按照类初始化所需的具体数据信息对类对象完成实例化工作。JVM也将向它分配内存,存放自身及由其父类继承下来的所有实例和变量。当给一些实例中的变量分配内存的同时,也先会被赋予一个默认值(零值)。之后会进行对象头的设置和填充。在这些阶段全部完成之后,JVM才会对该对象进行应用级别的属性参数设置。
注意:在实例化阶段,当填充默认值(零值)时,如果启用了TLAB模式,则填充零值会提前到TLAB阶段。
JVM对象实际内存数据结构,如图3.4所示,包括对象类的头部(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。
Mark Word(标记字段):包括哈希码、分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息。Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
如果元素是数组,还必须有一个数据区域用来存储该数组。因为没有任何方法可以从对象元素的数据中直接确定数组值的大小,所以必须将其保存在一个对象列表头的MarkWord中。MarkWord按照不同对象的存储状态位划分不同的存储状态位,以便区别不同的对象存储系统结构,如表3.1所示。
图3.4 对象的内存结构图
表3.1 标记字段结构
Mark Word所对应的内容取值选项根据不同的场景,主要有5种情况,如表3.2所示。当处于初始未锁定状态时,状态位为01,对应的Mark Word存储的是对象的哈希码和当时对象分代的年龄值;当有一个线程锁定该对象的时候会进入偏向锁状态(可偏向),此时标志位仍然为01,但是Mark Word存储的值会改为当前锁定该对象的线程ID,以及偏向时间戳(占用对象的时间戳)、当前该对象的分代年龄值;而当两个线程同时去争抢该对象资源的时候,会变更为轻量级锁状态,此时的状态位为00,对应的Mark Word存储的是执行锁对象的指针地址;当锁膨胀到重量级锁的时候,状态位会变更为10,Mark Word存储的是重量级锁的指针地址;当对象被回收了,那么状态位会跃迁为11,而Mark Word不会存储任何信息。
表3.2 MarkWord字段内容选项
类型指针(Klass Pointer):指向当前对象的类的元数据指针,虚拟机可以利用类型指针自动判断这个类的对象类型。并不是所有的虚拟机都必须在对象数据上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身。
实例数据(Instance Data):它是对象真正存储的有效信息,即程序代码中定义的各种类型的字段内容,无论是从父类继承的,还是在子类中定义的,都需要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
对齐填充(Padding):并不是必然存在的,其并没有特殊的含义,只起着填充占位功能。JVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。由于对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
JVM常见的访问定位方式主要有两种:句柄访问和直接指针访问。
句柄访问方式中,该句柄存放了指向实际实例对象的指针和指向数据类型的指针,其好处是当对象被移动时(如垃圾回收时,整理内存空间需要大量移动对象),不需要频繁地修改引用,只需要修改句柄中实例的数据指针即可,如图3.5所示。栈内引用对象的值存储了句柄对象地址,句柄池的对象存储了类型数据信息的地址和堆内存数据的地址。
图3.5 句柄池引用方式
直接指针访问,即将访问对象的一个引用值直接指向该访问对象地址,如图3.6所示。其主要优点是当通过引用指针访问一个对象时,不需要重新引用对象地址的定位,从而可以使对象访问速度更快。
图3.6 直接指针访问
此外,针对要访问的对象,要先考虑访问对象的类型,其中堆内存中引用对象存储的数据是该对象的内存地址,而非引用类型(基本类型)存储的是值。