类加载过程

一般来说,Java 的类加载过程分为三个主要步骤:加载、链接、初始化,细节部分在Java 虚拟机规范中有详细介绍。

Java 的语言类型可以分为基本类型和引用类型。基本类型是 Java 虚拟机预先定义好的。引用类型中,Java将其细分为:类、接口、数组类和泛型参数,泛型参数在编译过程中会被擦除(没看懂啥意思??),数组类是 JVM 直接生成的,另外两种则有对应的字节流。

最常见的字节流就是 Java 编译生成的 class 文件,Java 将来自不同数据源的字节流读取到 JVM 中生成类或接口就是类加载过程,这里的数据源可能是 jar 包、class文件等等。

加载

加载,是指查找字节流,并根据字节流创建类的过程。JVM 中有三种类加载器:启动类加载器、扩展类加载器、应用类加载器,每种类加载器加载一部分类。加载阶段,类的唯一性通过类加载器名称和类全面来共同确定,因此不同类加载器加载同一串字节流也会生成两个不同的类。

启动类加载器(Bootstrap Class Loader)加载最基础、最重要的类,比如 lib/ 目录下 jar 包中的类。

扩展类加载器(Extension Class Loader)的父-类加载器是启动类加载器,它负责加载 lib/ext/ 目录下的 jar 包,即所谓的 extension 机制。

应用类加载器(Application Class Loader)的父-类加载器是扩展类加载器,它负责加载应用程序路径下的类,这个应用程序路径一般是环境变量 classpath 指定的路径。

链接

链接是指将创建成的类合并到 JVM 中,使之能够被执行的过程,这是类加载的核心步骤。链接阶段又可分为验证、准备、解析三个子阶段。

验证阶段要核验字节信息是否符合JVM 规范,保证加载类能够满足JVM 的约束条件。

准备阶段,创建并为被加载的类的静态字段分配内存,注意,这里并不会做一般意义上的显式初始化,而只是创建变量并为变量开辟内存空间,做默认的初始化。例如我们 int a = 1,那么在链接的准备阶段,这个 a 的值是 0,并不会赋值为 1。

在准备阶段,还有一个重要的工作,就是为类构造相关的数据结构,例如实现动态绑定的方法表。在 class 文件被加载至 JVM 之前,一个类无法知道自己和其他类的其方法、字段所对应的具体地址。Java编译时会生成一个符号引用,通过符号引用来无歧义得定位到具体目标上。

举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

解析阶段就是要将符号引用解析为直接引用或实际引用。但是解析阶段的发生是不确定的,它可能在初始化步骤之前,也可能在初始化步骤之后。同时,在类加载的过程中,所有步骤、阶段是按顺序开始,但并不是一定按顺序进行或完成的。

初始化

类加载的最后一步是初始化,这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

JVM 规范枚举了下述多种触发情况:

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化;
  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射 API 对某个类进行反射调用时,初始化这个类;
  8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

只有初始化完成之后,一个类才能真正成为可执行的状态。

双亲委派

双亲委派模型,简单说就是当类加载器(Class Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。

NoClassDefFoundError 和 ClassNotFoundException

NoClassDefFoundError 在官方文档中解释是要使用的类的编译时还找得到,但是到了运行的时候找不到了,那么此时就会抛出这个异常。例如,我编译了一个类 B,在类 A 中调用类 B 的方法,但是编译之后我删除了类 B 的 class 文件,此时运行类 A 就会报错 NoClassDefFoundError。

ClassNotFoundException 情况就比较多了,当通过以下方法来加载类的时候,

  1. 类 Class 中的 forName() 方法
  2. 类 ClassLoader 中的 findSystemClass() 方法
  3. 类 ClassLoader 中的 loadClass() 方法

但是没有找到具体指定的名称的类的定义,就会报错 ClassNotFoundException。

从类加载过程来看 NoClassDefFoundError 和 ClassNotFoundException。在加载阶段,如果从 classpath 等路径中找不到所需的类,那么就会报错 ClassNotFoundException,除了上面的三种原因之外,还有可能是因为类被不同类加载器重复加载了导致的。而 NoClassDefFoundError 是在链接步骤中,从内存找不到所需的类,那么就会报错 NoClassDefFoundError。

总结就是,加载时从外存储器找不到需要的 class 就出现 ClassNotFoundException ,链接时从内存找不到需要的 class 就出现 NoClassDefFoundError 。