类的加载过程一般分为三个阶段
[JVM]class加载过程 - 图1

  • 加载阶段
  • 连接阶段
  • 初始化阶段

    一、类的加载阶段

负责查找并且加载 class 文件,并将加载的二级制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,
并在堆内存中生成一个该类的 java.lang.Class 对象,作为访问方法区数据结构的入口。如下图:
[JVM]class加载过程 - 图2

知识点

1、类加载的最终产物 堆内存中的class 对象

2、类的加载通过全限定名(包名 + 类名)来获取二进制数据流。

3、几种常见的 class 加载方式

  • a、运行时动态生成。如:动态代理时生成代理类的二进制字节流
  • b、通过网络获取,动态加载。
  • c、通过 zip 文件读取二进制字节流。如:jar、war。
  • d、将类的二进制数据存储在数据库的 BLOB 字段类型中
  • e、运行时生成 class 文件,并且动态加载。

4、对同一个 ClassLoader 来讲,不管某个类被加载了多少次,对应到堆内存中的 class 对象始终是同一个。

5、类加载的整个生命周期中,加载过程没结束,连接阶段是可以交叉工作的(如:连接阶段验证字节流信息的合法性),不过总的来说加载阶段肯定出现在连接阶段之前。

二、类的连接阶段

链接阶段操作的内容比较多,可以细分为如下三个阶段:

  • 验证
  • 准备
  • 解析

    1、验证

主要是确保 class 文件的字节流所包含的内容符合当前 JVM 规范,并且不会出现危害 JVM 自身安全的代码。相关验证信息如下:

a、验证文件格式

  • 主次版本号

Java 的版本是不断升级的,JVM 规范同样也在不断升级,高版本兼容低版本,但是低版本不兼容高版本,所以需要验证当前 class 文件版本是否符合当前 JDK 所处理的范围。

  • class 文件的完整性

每个类在编译阶段经过 MD5 摘要算法计算之后,都会将结果一并附加给 class 字节流作为字节流的一部分,这里主要验证 class 的 MD5 指纹。

  • 常量池中的常亮是否存在不被支持的变量类型。
  • 指向常量中的引用是否指到了不存在的常量或者该常量的类型不被支持。
  • 其他信息

b、元数据验证。

对 class 的字节流进行语义分析的过程,确保 class 字节流符合 JVM 规范的要求。

  • 检查是否存在父类,是否继承了某个接口,这些父类和接口是否合法,或者是否真实存在
  • 检查该类是否继承了被 final 修饰的类,被 final 修饰的类不允许被继承且其中的方法是不允许被 override 的。
  • 检查该类是否为抽象类,如果不是抽象类,那么它是否实现了父类的抽象方法或者接口中的所有方法。
  • 检查方法重载的合法性,如相同的方法名称,相同的参数但是返回类类型不同,这都是不被允许的。
  • 其他语义验证

c、字节码验证

当经过了文件格式和元数据的语义解析后,还要对字节码进一步进行验证,主要验证程序的控制流程,如:循环、分支等。

  • 保证当前线程在程序计数器中的指令不会跳转到不合法的字节码指令中
  • 保证类型的转换是否合法,比如用 A 声明的引用,不能用 B 进行强制类型转换。
  • 保证任意时刻,虚拟机栈中的操作栈类型与指令代码都能正确地被执行,如:在压栈的时候传入的是一个 A 类型的引用,在使用的时候却将 B 类型载入了本地变量表。
  • 其他验证

d、符号引用验证

在类的加载过程中,有些阶段是交叉进行的,比如在加载阶段尚未结束之前,连接阶段可能已经开始工作了,这样做的好处是能够提高类加载的整体效率,同样 符号引用的验证,其主要作用就是验证符号引用转换为直接引用时的合法性。保证解析动作的顺利执行,比如: 如果某个类的字段不存在,则会抛出 NoSuchFieldError,若方法不存在则抛出 NoSuchMethodError 等。

  • 通过符号引用描述的字符串全限定名称是否能能够顺利的找到相关的类
  • 符号引用的类、字段、方法,是否对当前类可见,比如:不能访问引用类的私有方法。
  • 其他

2、准备

当一个 class 文件的字节流通过了所有的验证过程之后,就开始为对象的类变量(静态变量)进行初始值设置。

各种类型默认初始值

数据类型 初始值
Byte (byte) 0
Char \u0000
Short (short) 0
Int 0
Float 0.0 F
Double 0.0 D
Long 0 L
Boolean False
引用类型 null
  • 为类的静态变量分配内存,并且初始化默认值
  • 如果类中存在常量直接进行赋值。

代码例子

  1. public class Demo{
  2. private static int a = 10 ;
  3. private final static int b = 10;
  4. }

在准备阶段 a=0 ,b=10

3、解析

在连接阶段中经历了 验证和准备之后,就进入到解析过程了,在解析过程中同样会交叉一些验证的过程,比如:符号引用的验证。 解析的工作就是在常量池中寻找类、接口、字段和方法的符号引用,并将这些符号引用替换成直接引用的过程。 解析过程只要针对类接口、字段、类方法和接口方法这四类进行的,分别对应到常量池中的 CONSTANT_Class_info、CONSTANT_Fielderf_info、Constant_Methodref_info 和 Constant_InterfaceMethodred_info 这四种类型常量。

三、类的初始化阶段

类的初始化阶段是整个类加载过程中最后一个阶段,在初始化阶段主要是执行 <clinit>() 方法。在 <clinit>() 方法中所有的类变量都会被赋予正确的值,也就是程序编写时指定的值。

<clinit>() 方法是编译阶段生成的,它包含在 class 文件中。

为类的静态变量赋予正确的初始值(代码编写阶段给定的值)

小贴士 JVM 对类的初始化是一个延迟的机制,只有当一个类被主动使用的时候类才会被初始化。

类的主动使导致类的初始化

JVM 虚拟机规范规定,每个类或者接口被 Java 程序 首次主动使用 时才会对其进行初始化。

通过 new 关键字会导致类的初始化。

访问类的静态变量,或者给静态变量赋值会导致类的初始化。

访问静态方法,会导致类的初始化

都某个类进行发射操作,会导致类的初始化。

初始化子类导致父类初始化。

启动类(如:spring boot 中的 Application.class),执行 main 函数所在的类会导致类的初始化。

构造某个类的数组并不会导致类的初始化

四、扩展:类的卸载

JVM 启动时指定 -verbose:class 可以观察到加载的 class

在 JVM 启动过程中,JVM会加载很多的类,在运行期间同样也会加载很多的类。

对象在堆内存中的 Class 对象以及 Class 在方法区中的数据结构在以下几种情况下会被 GC 回收,也就是类被卸载

  • 该类的所有实例都已经被 GC 。
  • 加载该类的 ClassLoader 实例被回收。
  • 该类的 class 实例没有在其他地方被应用。

当某个对象在堆内存中如果没有其他地方用则会在垃圾回收器线程进行 GC 的时候被回收掉