Java 虚拟机类加载过程解析

内容分享2天前发布
0 0 0

类的加载过程是 Java 虚拟机(JVM)运行时机制中极为关键的一环。它不仅关系到程序能否正确执行,还直接影响性能、安全性和扩展性。

本文将对 Java 类加载全过程详细解析


一、概述

类加载是指 JVM 将 .class 文件(或其他来源的字节码)加载到内存中,并对其进行校验、准备、解析和初始化,最终生成一个可被 JVM 直接使用的 java.lang.Class 对象的过程。

Java 虚拟机类加载过程解析

✅“类加载”在广义上包含五个阶段:
加载(Loading) → 验证(Verification) → 准备(Preparation) → 解析(Resolution) → 初始化(Initialization)
其中JVM 规范将“连接(Linking)”分为验证、准备、解析三个部分。

图中的使用和卸载是为了能看到 class 在虚拟机中的生命周期全貌而画出来的,本身已经不属于加载类加载阶段。


二、详细阶段分解

1. 加载(Loading)

目标

  • 获取类的二进制数据;
  • 在方法区(Metaspace / 永久代)中创建该类的运行时表明;
  • 在堆中创建对应的 java.lang.Class 对象。

具体任务

  1. 通过类的全限定名(Fully Qualified Name)获取其二进制字节流
    来源可以是:
  2. 本地文件系统(.class 文件)
  3. JAR、WAR、ZIP 等归档文件
  4. 网络(如 Applet、RMI)
  5. 数据库(少见)
  6. 动态生成(如 ASM、Javassist、CGLIB、Lambda 表达式内部类)
  7. 加密后的字节码(需自定义 ClassLoader 解密)
  8. 将字节流解析为 JVM 内部的数据结构
  9. 存储在 方法区(Method Area) 中(JDK 8+ 为 Metaspace,使用本地内存);
  10. 包含:类名、父类、接口列表、字段表、方法表、常量池、访问标志等。
  11. 在 Java 堆中创建一个 java.lang.Class 实例
  12. 该对象是程序访问类元数据的唯一入口;
  13. 所有反射操作都基于此对象;
  14. 同一个类在同一个类加载器下只会有一个 Class 对象。

特点

  • 非必须顺序执行:加载阶段可能与后续的验证阶段交叉进行(如边加载边验证);
  • 用户可控:可通过自定义 ClassLoader 重写 findClass() 或 loadClass() 控制加载逻辑。

2. 验证(Verification)

目标

确保 Class 文件的字节流不会危害 JVM 的安全性和稳定性。这是 JVM 安全体系的第一道防线。

⚠️ 若跳过验证(如 -Xverify:none),可能导致 JVM 崩溃或安全漏洞。

四个子阶段(按顺序)

(1) 文件格式验证(File Format Verification)

  • 检查字节流是否符合 Class 文件规范;
  • 示例检查项:
    • 魔数是否为 0xCAFEBABE;
    • 主/次版本号是否在当前 JVM 支持范围内;
    • 常量池中的常量类型是否合法;
    • access_flags 是否合理(如不能同时为 final 和 abstract)。

此阶段保证输入是“看起来像 Class 文件”的数据。

(2) 元数据验证(Metadata Verification)

  • 对字节码描述的语义进行分析;
  • 示例检查项:
    • 类是否有父类(除 Object 外);
    • 是否继承了 final 类;
    • 是否实现了所有抽象方法;
    • 字段/方法是否重载冲突;
    • 接口方法是否为 public abstract。

此阶段确保类的结构符合 Java 语言规范。

(3) 字节码验证(Bytecode Verification)

  • 最复杂、最耗时的阶段;
  • 通过数据流分析控制流分析验证字节码逻辑;
  • 示例检查项:
    • 操作数栈是否会溢出或下溢;
    • 局部变量表访问是否越界;
    • 方法调用参数类型是否匹配;
    • 类型转换是否安全(如 (String) obj 前是否做过 instanceof 检查?JVM 不强制,但会验证类型兼容性);
    • return 语句是否返回正确类型。

自 JDK 6 起引入 类型推断验证器(Type Checker),提升效率。

(4) 符号引用验证(Symbolic Reference Verification)

  • 发生在解析阶段之前;
  • 确保解析能正确完成;
  • 示例检查项:
    • 引用的类是否存在;
    • 引用的方法/字段是否可访问(思考包私有、protected 等);
    • 是否违反继承规则(如调用父类 private 方法)。

若验证失败,抛出 java.lang.VerifyError。


3. 准备(Preparation)

目标

为类变量(static 字段)分配内存并设置初始值(零值)

关键点

  • 仅针对 static 字段(实例字段在对象实例化时分配);
  • 内存分配在方法区(JDK 8+ 在 Metaspace);
  • 赋值为“零值”,不是代码中定义的值

示例说明

public class Example {
    public static int a = 100;
    public static final int b = 200;
    public static String c = "hello";
    public static Object d;
}

字段

准备阶段值

初始化阶段值

a

0

100

b

200

200(不变)

c

null

“hello”

d

null

null

为什么 b 是 200?
由于 static final 且是编译期常量(ConstantValue 属性),编译器会将其值直接写入 Class 文件的 ConstantValue 属性,JVM 在准备阶段就赋值。

❌ 若 static final 字段在运行时计算(如 new Object()),则仍按零值处理,初始化阶段赋值。


4. 解析(Resolution)

目标

将常量池中的 符号引用(Symbolic Reference) 转换为 直接引用(Direct Reference)

什么是符号引用?

  • 一组符号描述目标,与内存布局无关;
  • 如:com/example/Foo.bar:()V;
  • 存在于 Class 文件的常量池中(CONSTANT_Class、CONSTANT_Fieldref、CONSTANT_Methodref 等)。

什么是直接引用?

  • 指向目标的指针、偏移量或句柄;
  • 与 JVM 内存布局强相关;
  • 可直接定位到方法或字段。

解析类型

引用类型

解析内容

类/接口解析

将类符号引用解析为方法区中的类对象

字段解析

找到字段在类中的内存偏移量

类方法解析

找到方法在虚方法表(vtable)中的位置

接口方法解析

找到接口方法表(itable)中的条目

方法类型解析

用于 invokedynamic(如 Lambda)

解析时机

  • 静态解析:在类加载期间完成(如 invokestatic, getstatic);
  • 动态解析:在运行时完成(如 invokevirtual, invokeinterface);
  • JVM 允许“懒解析”(Lazy Resolution):首次使用时才解析。

注意:解析失败抛出 java.lang.IncompatibleClassChangeError 或其子类(如 NoSuchFieldError)。


5. 初始化(Initialization)

目标

执行类的 类构造器 <clinit>() 方法,完成 static 字段的显式赋值和 static 块的执行。

<clinit>()方法详解

  • 由编译器自动收集 所有 static 变量赋值语句static 代码块,按源码顺序合并生成;
  • 无参数、无返回值、不可被直接调用;
  • 线程安全:JVM 保证 <clinit> 只被执行一次;
  • 若类无 static 赋值或 static 块,则不生成 <clinit>。

示例

public class InitDemo {
    static {
        System.out.println("Static block 1");
    }
    public static int x = initX();
    static {
        System.out.println("Static block 2");
    }

    private static int initX() {
        System.out.println("initX called");
        return 42;
    }
}

<clinit> 等价于:

static {
    System.out.println("Static block 1");
    x = initX(); // 输出 "initX called"
    System.out.println("Static block 2");
}

初始化顺序规则

  1. 父类先于子类初始化(除非父类已初始化);
  2. 接口初始化独立:实现接口不会触发接口初始化,只有真正使用接口的 static 字段时才初始化;
  3. 多个线程同时初始化:JVM 会阻塞其他线程,直到第一个线程完成 <clinit>。

触发初始化的 7 种“主动引用”场景(JVM 规范规定)

  1. new 创建实例;
  2. 访问 非编译期常量 的 static 字段(如 Example.a);
  3. 调用 static 方法;
  4. 反射调用(Class.forName(“…”) 默认初始化);
  5. 初始化子类时,若父类未初始化,则先初始化父类;
  6. 虚拟机启动时指定的主类(含 main 方法);
  7. JDK 7+:MethodHandle 或 invokedynamic 首次解析。

不触发初始化的“被动引用”示例

class Parent {
    static {
        System.out.println("Parent initialized");
    }
    public static final int VALUE = 100; // 编译期常量
}

class Child extends Parent {
    static {
        System.out.println("Child initialized");
    }
}

// 测试
System.out.println(Child.VALUE); // 仅输出 100,不触发 Parent 或 Child 初始化!

由于 VALUE 是编译期常量,已被内联到调用类的常量池中。


三、类加载器(ClassLoader)体系

1. 内置类加载器

类加载器

加载路径

父加载器

实现语言

Bootstrap ClassLoader

<JAVA_HOME>/lib

无(null)

C++

Extension ClassLoader

<JAVA_HOME>/lib/ext

Bootstrap

Java

Application ClassLoader

-classpath

或 -cp 指定路径

Extension

Java

2. 双亲委派模型(Parent Delegation Model)

  • 工作流程
  • 收到类加载请求;
  • 先委托父加载器尝试加载;
  • 父加载器无法加载(未找到),自己才尝试加载。
  • 优点
    • 避免重复加载;
    • 保证核心类库安全(如无法自定义 java.lang.String);
    • 维护类的唯一性和一致性。

3. 打破双亲委派

  • 场景:SPI(Service Provider Interface)机制,如 JDBC、JNDI;
  • 方式:使用 线程上下文类加载器(Thread Context ClassLoader);ClassLoader cl = Thread.currentThread().getContextClassLoader();
    cl.loadClass(“com.mysql.cj.jdbc.Driver”);
  • ✅在作者前面的文章中有讲到类加载的双亲委派,可以作为参考。

四、类的卸载(Unloading)

  • 类卸载条件极其严格:
  • 该类的所有实例已被回收;
  • 加载该类的 ClassLoader 已被回收;
  • 该类的 java.lang.Class 对象没有在任何地方被引用。
  • 一般只在 自定义 ClassLoader + 动态加载 场景下发生(如热部署、OSGi);
  • 卸载后,下次使用会重新走完整加载流程。

五、常见QA

  • Q1:类加载和类初始化有什么区别?
    A:加载是把字节码读入内存并创建 Class 对象;初始化是执行 static 代码。
  • Q2:为什么 static final 常量在准备阶段就赋值?
    A:由于其值在编译期确定,存入 Class 文件的 ConstantValue 属性,JVM 直接读取。
  • Q3:数组类是如何加载的?
    A:由 JVM 自动生成,其类加载器与元素类型一样;无 <clinit> 方法。
  • Q4:如何查看类加载过程?
    A:使用 JVM 参数 -verbose:class 或 -XX:+TraceClassLoading。
© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...