Skip to content

类加载机制

1. 这是什么

类加载机制描述了 Java 类从字节码文件进入 JVM,到可以被程序使用的全过程。
它是理解启动流程、插件机制、隔离加载和框架扩展的重要基础。

一句话理解:

  • 源码编译成 .class 只是第一步
  • 只有类被 JVM 正确加载、链接、初始化后,程序才能真正使用它

2. 为什么重要

如果不理解类加载,很多问题会显得非常神秘,例如:

  • 重复加载
  • 类冲突
  • ClassNotFoundException
  • NoClassDefFoundError
  • 静态代码块为什么突然执行

理解类加载之后,这些问题会清晰很多。

3. 先建立直觉:类加载和类初始化不是一回事

这是本章最重要的第一层理解。

很多人会把下面这些动作混成一件事:

  • 类被找到
  • 类被加载进 JVM
  • 类元数据准备好
  • 静态代码块执行
  • 静态变量赋值

但实际上:

  • 类加载不等于类初始化
  • 你拿到 SomeClass.class 时,不一定已经执行了静态初始化逻辑

4. 核心内容

4.1 类从字节码到可用,大致经历哪些阶段

最常见的学习顺序是:

  • 加载(Loading)
  • 链接(Linking)
  • 初始化(Initialization)

而“链接”又可继续分成:

  • 验证
  • 准备
  • 解析

所以常见完整路径是:

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化

4.2 每个阶段可以怎么理解

加载

把类的字节码读进来,并生成对应的 Class 对象。

验证

确认字节码是否合法、安全、符合 JVM 规范。

准备

为类变量分配内存,并设置初始默认值。

解析

把符号引用转换成直接引用。

初始化

真正执行类构造逻辑,例如:

  • 静态变量显式赋值
  • 静态代码块

这一步最容易被直观感知,因为你经常会看到打印输出或副作用。

4.3 哪些操作会触发类初始化

常见会触发类初始化的动作包括:

  • new 一个类
  • 访问类的静态变量(非编译期常量)
  • 调用类的静态方法
  • Class.forName(...) 默认初始化

而下面这些动作不一定触发初始化:

  • 使用类字面量 SomeClass.class
  • 访问编译期常量

4.4 双亲委派模型是什么

双亲委派模型的核心思路是:

  • 类加载请求先向上委托给父加载器
  • 父加载器无法完成时,子加载器再自己尝试

你可以把它理解成:

  • “先问上级能不能处理,处理不了我再自己来”

4.5 双亲委派为什么重要

它的价值主要体现在:

  • 保证核心类库的一致性
  • 避免重复加载基础类
  • 提升安全性

例如:

  • 不能轻易自己写一个假的 java.lang.String 来替代系统类

4.6 常见类加载器层次

常见可以这样理解:

  • Bootstrap ClassLoader:加载核心类库
  • Platform ClassLoader:加载平台相关类库
  • AppClassLoader:加载应用类路径中的类

日常最常接触到的是:

  • AppClassLoader

4.7 自定义类加载器通常用来做什么

常见场景包括:

  • 插件化加载
  • 热部署
  • 隔离不同模块依赖
  • 自定义字节码来源

但它不是“平时业务开发一定要写”的常规工具。
学习阶段更重要的是理解:

  • 类加载器不同,加载出来的类在 JVM 看来就可能不是“同一个类”

4.8 ClassNotFoundExceptionNoClassDefFoundError 的区别

这两个很容易混淆。

ClassNotFoundException

通常出现在:

  • 你主动按名字去加载类,但 JVM 找不到它

例如:

java
Class.forName("com.example.MissingClass")

NoClassDefFoundError

通常出现在:

  • 类在编译时是存在的
  • 但运行时无法完成定义或初始化
  • 或者之前初始化失败,后续再次使用时出错

它们都和“类没正常可用”有关,但触发语义不同。

5. 学习重点

这一章真正要掌握的是:

  • 类加载和类初始化不是同一件事
  • 双亲委派解决的是安全和一致性问题
  • 类加载器不同,类身份也可能不同
  • ClassNotFoundExceptionNoClassDefFoundError 语义不同
  • 初始化触发时机是排障和框架理解的关键

6. 常见问题

6.1 混淆 ClassNotFoundExceptionNoClassDefFoundError

一个偏“主动加载找不到”,一个偏“定义或初始化过程出了问题”。

6.2 只记双亲委派流程,不理解设计意义

如果不知道它为什么存在,就很难真正理解类隔离、安全和核心类库稳定性。

6.3 忽视多类加载器环境下的隔离问题

在插件、容器、应用服务器场景里,这个问题会非常真实。

7. 动手验证

这一节可以直接复制运行,边看边验证。

7.1 准备一个可运行示例

新建文件 ClassLoadingDemo.java,内容如下:

java
public class ClassLoadingDemo {
    static class InitTarget {
        static int value;

        static {
            value = 123;
            System.out.println("InitTargetInitialized=true");
        }
    }

    static class BrokenInit {
        static int value = create();

        private static int create() {
            throw new RuntimeException("init failed");
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("appClassLoader=" + ClassLoadingDemo.class.getClassLoader().getClass().getSimpleName());
        System.out.println("platformClassLoader="
                + ClassLoader.getPlatformClassLoader().getClass().getSimpleName());

        Class<?> literal = InitTarget.class;
        System.out.println("literalLoaded=" + (literal != null));
        System.out.println("beforeForNameNoManualOutputYet=true");

        Class<?> byName = Class.forName("ClassLoadingDemo$InitTarget");
        System.out.println("forNameLoaded=" + (byName != null));
        System.out.println("staticValue=" + InitTarget.value);

        try {
            Class.forName("missing.NotExisting");
        } catch (ClassNotFoundException e) {
            System.out.println("classNotFoundCaptured=true");
        }

        try {
            System.out.println(BrokenInit.value);
        } catch (Throwable e) {
            System.out.println("firstBrokenInit=" + e.getClass().getSimpleName());
        }

        try {
            System.out.println(BrokenInit.value);
        } catch (Throwable e) {
            System.out.println("secondBrokenInit=" + e.getClass().getSimpleName());
        }
    }
}

7.2 编译并运行

bash
javac ClassLoadingDemo.java
java ClassLoadingDemo

如果你在 PowerShell 中操作,也直接执行同样两条命令即可。

7.3 你应该观察到什么

输出不一定逐字完全一致,但应包含这些关键信息:

text
appClassLoader=AppClassLoader
platformClassLoader=PlatformClassLoader
literalLoaded=true
beforeForNameNoManualOutputYet=true
InitTargetInitialized=true
forNameLoaded=true
staticValue=123
classNotFoundCaptured=true
firstBrokenInit=ExceptionInInitializerError
secondBrokenInit=NoClassDefFoundError

7.4 每一行在验证什么

  • literalLoaded=true 且初始化输出还没出现:说明拿类字面量不等于一定触发初始化
  • InitTargetInitialized=true:说明 Class.forName 默认会触发初始化
  • staticValue=123:说明初始化后静态变量已按逻辑赋值
  • classNotFoundCaptured=true:说明按名字主动加载缺失类会触发 ClassNotFoundException
  • firstBrokenInit=ExceptionInInitializerError:说明类初始化失败时,第一次通常体现为初始化异常
  • secondBrokenInit=NoClassDefFoundError:说明初始化失败后再次使用该类,会进入定义不可用状态

7.5 再做两个延伸验证

你可以继续做下面两个实验:

  1. Class.forName("ClassLoadingDemo$InitTarget") 改成只访问 InitTarget.class
  2. InitTarget 增加一个 static final int CONST = 1;,然后只读取这个编译期常量

你可以观察:

  • 类加载和初始化是可以拆开的
  • 编译期常量的访问语义和普通静态字段不同

8. 练习建议

下面这些练习做完,这一章会更扎实:

  • 自己画一张类加载阶段图
  • 验证哪些操作会触发类初始化
  • 区分总结 ClassNotFoundExceptionNoClassDefFoundError
  • 阅读一个使用自定义类加载器的插件示例

9. 自测问题

  • 双亲委派模型为什么重要?
  • 类加载和类初始化有什么区别?
  • Class.forNameSomeClass.class 的行为差别是什么?
  • ClassNotFoundExceptionNoClassDefFoundError 的触发语义有什么区别?
  • 自定义类加载器通常用于哪些场景?

10. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • 类加载包含加载、链接、初始化等阶段
  • 初始化是静态逻辑真正执行的关键步骤
  • 双亲委派保证核心类库的一致性与安全性
  • 类加载器不同可能导致同名类身份不同
  • ClassNotFoundExceptionNoClassDefFoundError 不应混为一谈