Java虚拟机是如何加载Java类的?
Java 虚拟机中的类加载即从 class 文件到内存中的类,按先后顺序需要经过加载、链接以及初始化三大步骤。
虚拟机的加载对象是什么?
上文中说过Java中有两种类型:基本类型和引用类型,而基本类型是由虚拟机预先定义好的,引用类型中的泛型参数又会在编译过程中被擦除,所以加载的对象就剩下类、接口和数组类。
在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流。无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要对其进行链接和初始化。接下来,就详细介绍一下每个步骤具体都在干些什么。
虚拟机的加载流程是什么?
1.加载
是指查找字节流,并且据此创建类的过程。上面提过数组类是由Java虚拟机直接生成的,所以加载过程针对的是生成字节流的类与接口。如何找到这些字节流,则需要虚拟机借助类加载器。
启动类加载器是由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。在 Java 9 之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由 Java 核心类库提供。故除了启动类加载器之外,其他的类加载器都是java.lang.ClassLoader 的子类,因此有对应的 Java 对象。
扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
Java 9 引入了模块系统,并且略微更改了上述的类加载器1。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。当然还可以自定义类加载器哦。
除了加载功能之外,类加载器还提供了命名空间的作用,在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
2.链接
是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
- 验证阶段:确保被加载类能够满足 Java 虚拟机的约束条件。
- 准备阶段:为被加载类的静态字段分配内存,构造其他跟类层次相关的数据结构。
- 解析阶段:将符号引用解析成为实际引用(Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析)。
符号引用则是在 class 文件被加载至 Java 虚拟机之前,类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。
3.初始化
初始化即给常量赋值以及执行 < clinit > 方法的过程,完成之后,类才正式成为可执行的状态。
类初始化触发条件
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类;
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
总结
虚拟机加载Java类是Java 虚拟机将字节流转化为 Java 类的过程。这个过程可分为加载、链接以及初始化三大步骤。
加载
:是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。链接
:是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。初始化
:是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。