JVM 每个类加载阶段分别做了什么?

发布于:2025-03-27 ⋅ 阅读:(28) ⋅ 点赞:(0)

JVM 类加载过程是 Java 虚拟机 (JVM) 将 .class 文件中的字节码加载到内存中,并使其可以在程序运行时使用的过程。这个过程主要分为五个阶段:加载 (Loading)链接 (Linking) (又细分为验证 (Verification)、准备 (Preparation)、解析 (Resolution)) 和 初始化 (Initialization)。 虽然规范中还提到 使用 (Using)卸载 (Unloading) 阶段,但这两个阶段通常不被视为类加载的 核心 阶段,且卸载在实际应用中发生得很少,尤其是在 HotSpot VM 等主流虚拟机上。

下面详细解释每个阶段的主要工作:

1. 加载 (Loading)

  • 目的: 找到并读取类的字节码,创建 java.lang.Class 对象。
  • 主要工作:
    • 通过类的全限定名获取定义此类的二进制字节流。 这个字节流可能来自多种来源,例如:
      • 本地文件系统中的 .class 文件 (最常见)
      • JAR 或 WAR 包中的 .class 文件
      • 网络 (例如,Applet 或 Web Start 应用)
      • 运行时动态生成 (例如,动态代理)
      • 数据库等其他存储介质
      • 由其他字节码生成器生成 (例如,JSP 编译后的 .class 文件)
    • 将这个字节流所代表的静态存储结构转化为方法区 (Method Area) 的运行时数据结构。 方法区是 JVM 运行时数据区的一部分,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 在内存中生成一个代表这个类的 java.lang.Class 对象。 这个 Class 对象作为程序访问方法区中这个类的各种数据的入口。注意,Class 对象是 在方法区 而不是在 Java 堆中实例化的,尽管它也是对象。

加载阶段: 简单来说,加载阶段就是把类的字节码从各种来源加载到 JVM 的方法区,并在方法区中创建该类的运行时数据结构,同时在内存中创建一个 Class 对象作为入口。

2. 链接 (Linking)

链接阶段负责将加载到 JVM 中的二进制字节码进行校验、准备和解析,使其成为可以被 JVM 执行的运行时环境的一部分。链接阶段又细分为三个子阶段:

  • 2.1 验证 (Verification)

    • 目的: 确保 Class 文件的字节流中包含的信息符合当前虚拟机的规范,并且不会危害虚拟机自身的安全。这是链接阶段最重要,但耗时也最长的阶段。
    • 主要工作: 验证从字节码流中读取的信息是否符合规范,包括:
      • 文件格式验证: 验证字节流是否符合 Class 文件格式规范 (例如,魔数、版本号等)。
      • 元数据验证: 对类的元数据信息进行语义校验,例如:
        • 是否有父类 (除了 java.lang.Object)
        • 父类是否被 final 修饰
        • 抽象类是否实现了所有接口或抽象方法
        • 字段、方法是否与父类冲突
      • 字节码验证: 这是最复杂的验证阶段,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如:
        • 类型转换是否合法
        • 栈帧中的数据类型与指令操作的数据类型是否匹配
        • 方法调用是否合法
        • 字节码指令序列不会导致程序跳转到不期望的位置
      • 符号引用验证: 发生在解析阶段之前,验证符号引用中通过字符串描述的全限定名是否能够找到对应的类、字段、方法,以及访问性 (public, private 等) 是否正确。

    验证阶段: 验证阶段的核心是确保加载的类是安全的、合法的,防止恶意或错误的字节码破坏 JVM 的稳定性和安全性。如果验证失败,将会抛出 VerifyError 异常或其子类异常。

  • 2.2 准备 (Preparation)

    • 目的: 为类变量 (static 变量,不包括实例变量) 分配内存并设置类变量初始值。
    • 主要工作:
      • 在方法区中为类变量分配内存空间。
      • 将类变量初始化为默认初始值 (零值)。 例如,int 类型变量初始化为 0boolean 类型变量初始化为 false,引用类型变量初始化为 null
      • 注意: 这里 会执行类变量的显式初始化 (例如,static int a = 123; 中的 123) 和静态代码块的执行。这些会在 初始化 阶段完成。
      • final static 修饰的且在编译期可知的字段 (常量) 会在准备阶段被赋予代码中设定的初始值。 例如,static final int CONSTANT = 123;,在准备阶段 CONSTANT 就会被赋值为 123

    准备阶段: 准备阶段主要为类变量分配内存并赋予默认初始值,为后续的初始化阶段做准备。

  • 2.3 解析 (Resolution)

    • 目的: 将常量池中的符号引用替换为直接引用。
    • 主要工作:
      • 符号引用: 符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义地定位到目标即可。例如,一个方法的全限定名、字段描述符等。
      • 直接引用: 直接引用是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。
      • 解析过程: 虚拟机将常量池中的符号引用替换为实际的内存地址或者句柄等直接引用,以便程序在运行时能够真正访问到引用的目标 (类、字段、方法等)。
      • 解析时机: 解析可以在类加载的 初始化阶段之前之后 开始。
        • 静态解析 (Eager Resolution): 在类加载的解析阶段就完成解析。例如,invokestatic, invokespecial 等指令所使用的符号引用通常会在解析阶段进行解析。
        • 动态解析 (Lazy Resolution): 在程序 真正使用 到符号引用时才进行解析。例如,invokevirtual, invokeinterface, invokedynamic 等指令所使用的符号引用可能会延迟到实际运行时才解析 (动态链接)。

    解析阶段: 解析阶段将 Class 文件中用符号引用的类、接口、字段、方法等替换为直接引用,使得程序在运行时能够找到并使用这些资源。

3. 初始化 (Initialization)

  • 目的: 执行类构造器 <clinit>() 方法,完成类变量的显式初始化和静态代码块的执行。
  • 主要工作:
    • 执行类构造器 <clinit>() 方法。 <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 (static{}) 中的语句合并产生的。
    • <clinit>() 方法与实例构造器 <init>() 方法不同。 <clinit>() 是类构造器,负责类的初始化;<init>() 是实例构造器,负责对象的初始化。
    • <clinit>() 方法的执行特点:
      • JVM 会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕。 因此,第一个被执行 <clinit>() 方法的类一定是 java.lang.Object
      • <clinit>() 方法对于类或接口来说不是必需的。 如果一个类没有静态变量赋值操作,也没有静态代码块,那么编译器可以不为这个类生成 <clinit>() 方法。
      • 接口中也可以有类变量初始化操作,但接口的 <clinit>() 方法与类的 <clinit>() 方法略有不同。 执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量被使用时,父接口才会初始化。
      • JVM 会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁同步。 这意味着如果多个线程同时去初始化一个类,只会有一个线程执行该类的 <clinit>() 方法,其他线程会被阻塞等待,直到 <clinit>() 方法执行完毕。这保证了类的初始化过程是线程安全的。

初始化阶段: 初始化阶段是类加载过程的最后一步,也是真正开始执行类中定义的 Java 代码的阶段。它负责执行类构造器 <clinit>() 方法,完成类变量的显式初始化和静态代码块的执行,使类真正可以使用。

4. 使用 (Using)

  • 目的: 类被程序代码正常使用,例如创建类的实例,访问类的静态成员变量,调用类的静态方法等。
  • 主要工作: 这个阶段就是程序代码按照正常的逻辑使用已经加载、链接和初始化完成的类。

5. 卸载 (Unloading)

  • 目的: 当一个类不再被任何地方引用时,JVM 可能会在方法区中卸载该类。
  • 条件: 类卸载的条件非常苛刻,通常只有在以下情况才可能发生 (但实际应用中非常罕见,尤其是在 HotSpot VM 中):
    • 该类的所有实例都已经被回收。 (包括 java.lang.Class 对象本身)
    • 加载该类的类加载器已经被回收。
    • 该类对应的 java.lang.Class 对象没有任何地方被引用 (包括反射、JNI 等)。
  • 实际情况: 在 HotSpot VM 中,默认情况下,类卸载的条件非常苛刻,几乎不可能发生。只有在某些特定的场景下,例如使用自定义类加载器,并且程序主动进行类卸载操作时,才有可能卸载类。 在大多数情况下,类一旦被加载到 JVM 中,就会一直存在,直到 JVM 进程结束。

类加载阶段:

类加载是一个复杂而重要的过程,它保证了 Java 程序可以动态加载类,并确保了程序的安全性和正确性。 理解类加载机制对于深入理解 JVM 的运行原理和进行性能调优至关重要。

简要流程概括:

  1. 加载 (Loading): 找到字节码,读取到内存,创建 Class 对象。
  2. 链接 (Linking):
    • 验证 (Verification): 确保字节码合法安全。
    • 准备 (Preparation): 为类变量分配内存并赋默认初始值。
    • 解析 (Resolution): 将符号引用替换为直接引用。
  3. 初始化 (Initialization): 执行 <clinit>() 方法,完成类变量的显式初始化和静态代码块。
  4. 使用 (Using): 程序正常使用类。
  5. 卸载 (Unloading): (很少发生) 当类不再被引用时,可能被卸载。

网站公告

今日签到

点亮在社区的每一天
去签到