类加载的过程:加载、验证、准备、解析、初始化,一个类的生命周期
类的生命周期概述
Java程序的所有数据结构和算法都封装在类型之中,这也是面向对象编程语言的一大特色。当JVM执行一个Java类所封装的算法之前 ,首先要做的一件事便是字节码文件解析,字节码文件解析包含 3 个主要的过程:常量池解析、Java类字段解析及 Java 方法解析。通过类字段解析,JVM能够分析出Java类所封装的数据结构;通过方法解析,JVM能够分析出Java类所封装的算法逻辑 。而无论是数据结构还是方法信息,很多与“字符串”或者大数据(是指占二进位比较多的大数)相关的信息都封装于常量池中,所以JVM欲解析字段和方法信息,必先解析常量池。当常量池、字段和方法信息全部被解析完,则字节码文件的“精华”便已经被完全消化吸收。但是,这几个过程其实仅仅属于Java类 “加载”过程中的一个环节,这对于一个Java类的整个“漫长”的生命周期而言,仅仅是个开始。在字节码文件的精华被吸收之后还需要经过一系列的“二次” 消化处理,方能被JVM在运行期“随心所欲”地调用。
按照JVM规范,一个Java文件从被加载到被卸载的整个生命过程,总共要经历5个阶段 : 加载 → 链接(验证+准备+解析)→ 初始化(使用前的准备)→ 使用 → 卸载。其中第二个阶段“链接”,对应了3个阶段 :验证 、准备和解析,因此,也有很多典籍说 Java 类的生命周期一共包括7个阶段。
前文所讲的常量池解析、Java字段和方法的解析,其实都属于加载阶段的一部分。所谓加 载,简而言之就是将 Java 类的字节码文件加载到机器内存中并在内存中构建出Java类的原型类模板对象。所谓类模板对象,其实就是 Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则只叫在运行期也无法反射。字节码相关的工具类库,例如 asm 、cglib 等,都利用了这一机制,在运行期动态修改静态声明的 Java 类所对应的字节码内容,从而在运行期直接改掉Java 类的定义,甚至直接在运行期创建一个全新的Java类。
Java类是写给人类看的,而只叫内存中的类模板快照则是写给机器“看”的。物理机器无法直接执行Java类的源代码,所以需要通过类加载这样一个过程将字节码格式的 Java 类转换成机器能够识别的内存类模板快照。
JVM完成 Java 类加载之后,接着便开始进行链接。所谓链接,虽然与编译原理中的链接不是同一件事,然而本质上是相同的。总体而言,链接的主要作用是将字节码指令中对常量池中的索引引用转换为直接引用。链接包含 3 个步骤 :验证、准备和解析。其实在类加载阶段(也即类的生命周期的第一个阶段)JVM会对字节码文件进行验证,只不过该阶段的验证着重于字节码文件格式本身,与“链接”阶段的验证侧重点不同。在链接阶段,着重于由字节码信息出发进行反向验证,例如,验证根据字节码文件中的类名是否能够找到对应的类模板。这些都验证无误之后,JVM才能放心地加载当前类,也才能放心地将字节码指令中对常量池索引号的引用重写为直接引用。
在正式使用Java类之前的最后一道工序便是“初始化”,这里所谓的初始化,并非指对类进行实例化,而是指执行类的()方法。Java类的实例化,对应的乃是 Java 类 生命周期中的”使用”段。总体而言,当Java类中包含 static 修饰的静态字段 ,或者有使用 static{}块包裹的代码段时,编译后便会在字节码文件中包含一个名为()的方法,JVM在初始化阶段便会调用该方法。需要说明一点,该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法 虽,该方法也是由字节码指令所组成。
等JVM完成类的初始化之后,便“万事俱备,只欠东风”,就等着开发者使用了。使用方式多种多样,其中最常见的一种方式是通过new关键字来实例化一个 Java 类。
当然,除了通过new关键字使用java类,还有多种方式,例如下面的例子:
该示例使用Class.forName(String)接口加载一个类,并通过Class.newlnstance()接口实例化一个类。
从广义上说,类的加载也可以对应类生命周期的7个阶段中的前 5个阶段即加载、验证 、 准备 、解析和初始化。当类加载之后,JVM内部会为Java类创建一个对等的类模板,类模板在JDK 6时代被存储在所谓的perm区,而到了JDK 8时代,则被存储在所谓的metaSpace 区。无论存储在哪里,当存储区即将被打爆而这个类又不再使用时,JVM的GC便有可能将其回收,即释放内存。而当实例化一个 Java 类之后 ,JVM内部则会为Java类实例对象创建一个对等的实例对象,该实例对象所存储的区域与具体的 GC 策略紧密关联,有可能在新生代,也可能在老年代,当然,更可能在栈上(栈上分配)。当类被使用完毕之后,JVM必须销毁实例对象,否则只怕内存区早晚会被打爆。JVM对类模板的销毁和类实例对象的销毁,都是卸载。
总体而言,Java类的生命周期如图所示。
类加载
前文已经描述过JVM对字节码文件的精华部分的解析过程,当字节码文件解析完成之后,JVM便会在内部创建一个与Java类对等的类模板对象,说白了该对象其实是 C++类的实例。每一个Java类模型,最终在只叫内部都会有一个 klassOop 与之对等 ,Java 类中的字段 、方法及 常量池等都会保存到 klassOop 实例对象中。要注意,这个实例对象并非 Java 类的实例对象,其仅仅用于表示 Java 类型本身,或者 Java 类的定义。与 Java 类实例对象对等的JVM内部对象是instanceOop实例。
下面就从Java类模板对象instanceKlass 的创建开始讲起。
前面描述过Java字节码文件的常量池解析 、字段解析与方法解析 ,这三部分内容的解析便是 Java 字节码文件的精华所在。当这 3 个过程执行完成之后,Java字节码文件的精华便被分析完了,至此JVM便对Java类中所定义的一切数据结构和算法“了如指掌”,为了巩固“胜利成果”,JVM需要将这些好不容易辛辛苦苦解析出来的结果保存起来。这些解析的结果会存储到klassOop这个内部类对象实例中,可以将该对象看作 Java 类在JVM内部完全对等的一个镜像,只不过Java类是写给人类看的,而内部镜像 klassOop 则是写给机器读的。当成功保存解析结果之后,则 Java 类的生命周期的第一个阶段加载,便大功告成。不看过程看结果,类加载阶段其实就是为了这一目标而来,在JVM内部创建一个与Java类结构对等的数据对象。
从instanceKlass的结构可以看到,其内部定义了若干字段,这些字段足以存储 Java 类规范所支持的一切信息,例如字段 、方法 、内部类等,因为 instanceKlass 要作为 Java 类在JVM内部对等的结构体,所以能够兼容Java类中的所有元素是其唯一的设计目标。但是 JVM在创建instanceKlass对象时,为其所申请的内存空间却超过了instanceKlass 类型本身所需的内存大小,这是因为JVM需要在instanceKlass内存空间的末尾再预留出足够的空间,存储虚方法表 vtable接口表 itable 及 JAVA 类中的引用类型表 oopMap。存储虚方法表 vtable,其作用在前文分析Java
方法的解析机制时详细描述过,这里不再赘述。itable与 oopMap 也是各有其作用。不过静态字段在不同的 JDK 版本中存放的位置不同,在JDK 6中,静态字段会被分配到 instanceKlass 实例对象所申请的内存空间中,而在 JDK7和JDK8中,静态字段将会被分配到与 instanceKlass对等的镜像类一 java .lang.Class 实例中,关于静态字段的分配及镜像类会在下文详细分析,此处先略过不表。
图中的这个内存数据结构,便是Java类加载的最终产物,也是Java类在内存中的对等体。JVM根据这个数据结构,能够获取Java类中所定义的一切元素。
类加载的最终结果便是在JVM的方法区创建一个与Java类对等的instanceKlass 实例对象,但是在JVM创建完instanceKlass之后,又创建了与之对等的另一个镜像类java.lang.Class。
JVM之所以在instanceKlass之外再创建一个mirror,是有用意的,总体而言,java.lang. Class是为了被 Java 程序调用,而 instanceKlass 则是为了被JVM内部访问。所以,JVM直接暴露给Java的是 java_mirror,而不是 InstanceKlass 。
JDK8之所以将静态字段从instanceKlass 迁移到mirror中,也不是没有道理。毕竟静态字段并非Java类的成员变量,如果从数据结构这个角度看,静态字段不能算作 Java 类这个数据结构的一部分,因此 JDK 8 将静态字段转移到 mirror 中。从反射的角度看,静态字段放在 mirror 中是合理的,毕竟在进行反射时,需要给 出 Java 类中所定义的全部字段,无论字段是不是静态类型。