JVMNotes

第 03 章:类加载

zjc 于 2026-01-03 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 类加载决定类从哪里来、何时初始化、由谁隔离、初始化执行什么。生产中常见的 NoClassDefFoundError、ClassNotFoundException、LinkageError、同名类冲突、热部署失败和 Agent 干扰,都要回到类加载机制分析。

3.1 类生命周期

Loading 加载
  -> Verification 验证
     -> Preparation 准备
        -> Resolution 解析
           -> Initialization 初始化
              -> Using 使用
                 -> Unloading 卸载

各阶段职责:

阶段 职责
Loading 读取字节流,生成运行时表示
Verification 校验格式、语义和安全
Preparation 静态字段分配内存并设零值
Resolution 常量池符号引用转直接引用
Initialization 执行 <clinit>

验证、准备、解析常合称为 Linking。

3.2 类初始化

public class Config {
    public static final int VERSION = 1;
    public static int counter = initCounter();

    static {
        System.out.println("Config static block");
    }

    private static int initCounter() {
        return 10;
    }
}

准备阶段:

counter = 0

初始化阶段:

counter = initCounter()
执行静态代码块

触发初始化的常见场景:

  1. new 对象;
  2. 访问非 final 静态字段;
  3. 调用静态方法;
  4. 反射调用;
  5. 初始化子类时先初始化父类;
  6. main 所在类。

不触发初始化的示例:

System.out.println(Config.VERSION);

编译期常量会在使用处内联,具体行为以编译结果和规范为准。

3.3 类加载器层次

Java 9 之后:

Bootstrap ClassLoader
  -> Platform ClassLoader
     -> Application ClassLoader
        -> Custom ClassLoader

查看:

System.out.println(String.class.getClassLoader());
System.out.println(java.sql.Connection.class.getClassLoader());
System.out.println(App.class.getClassLoader());
加载器 加载内容
Bootstrap 核心类库
Platform 平台模块
Application classpath / modulepath 应用类
Custom 业务隔离、热部署、插件

Bootstrap 在 Java 9 后可能显示为 null 或由内部实现表示,不应依赖输出字符串判断。

3.4 双亲委派

流程:

Custom Loader
  -> Application
     -> Platform
        -> Bootstrap
           -> 找不到再逐层向下

价值:

  1. 核心类库不被随意替换;
  2. 避免重复加载;
  3. 保证类型一致性;
  4. 提升安全性。

破坏场景:

  1. Web 容器隔离应用;
  2. SPI 需要父加载器访问子实现;
  3. OSGi 模块化;
  4. 热部署;
  5. 测试 Mock;
  6. Java Agent 增强。

破坏双亲委派不是坏设计,但必须明确隔离边界和生命周期。

3.5 SPI 与线程上下文类加载器

JDBC 示例:

DriverManager.getConnection(
    "jdbc:postgresql://db:5432/app",
    "app",
    "password"
);

服务加载:

ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
    System.out.println(driver.getClass().getName());
}

问题:核心模块中的 DriverManager 需要加载应用 classpath 的驱动。

解决思路:使用线程上下文类加载器。

查看:

Thread.currentThread().getContextClassLoader()

线程池会复用线程,任务中修改上下文类加载器必须谨慎。

3.6 常见异常

异常 含义
ClassNotFoundException 显式加载时找不到类
NoClassDefFoundError 编译时存在,运行时链接失败
LinkageError 链接一致性失败
ClassCastException 类型转换失败
UnsatisfiedLinkError native 库找不到或不匹配

排查:

jar tf app.jar | grep 'com/example/Missing.class'
find ~/.m2/repository -name '*.jar' | xargs grep -l 'Missing.class'

排查方向:

  1. jar 是否打包;
  2. classpath 顺序;
  3. 版本冲突;
  4. 类加载器隔离;
  5. 初始化失败后的二次访问;
  6. 模块系统 exports;
  7. 动态代理生成失败。

3.7 类冲突诊断

查看类来源:

Class<?> clazz = Class.forName("com.example.Foo");
System.out.println(clazz.getProtectionDomain().getCodeSource().getLocation());

Arthas:

sc -d com.example.Foo

Maven 依赖树:

mvn dependency:tree -Dincludes=com.fasterxml.jackson.core

Gradle:

gradle dependencies --configuration runtimeClasspath

常见冲突:

风险
Jackson 版本不匹配导致序列化失败
Guava 内部 API 变化
Log4j / Logback 绑定冲突
Netty 多版本共存
ASM 字节码版本不支持新 class

3.8 自定义类加载器

public class DirectoryClassLoader extends ClassLoader {
    private final Path base;

    public DirectoryClassLoader(Path base, ClassLoader parent) {
        super(parent);
        this.base = base;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = Files.readAllBytes(base.resolve(name.replace('.', '/') + ".class"));
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }
}

使用:

ClassLoader loader = new DirectoryClassLoader(
        Path.of("/tmp/classes"),
        Thread.currentThread().getContextClassLoader()
);
Class<?> clazz = Class.forName("com.example.Plugin", true, loader);
Object plugin = clazz.getConstructor().newInstance();

注意:

  1. 类名和字节码一致;
  2. defineClass 只能调用一次;
  3. 包权限和签名;
  4. parent 选择;
  5. 资源加载;
  6. 卸载条件。

3.9 热加载与卸载

类卸载条件较苛刻:

1. 该 Class 的所有实例已回收
2. 加载它的 ClassLoader 已回收
3. Class 对象没有其他引用

热部署常见问题:

  1. 旧类实例仍存在;
  2. 静态状态不重置;
  3. 类加载器泄漏;
  4. Metaspace 增长;
  5. 框架缓存旧类;
  6. 线程持有旧 ClassLoader。

开发环境热替换与生产全量发布是不同问题,不要混用。

3.10 Java 模块系统

Java 9 引入 JPMS。

module-info.java

module com.example.order {
    requires java.sql;
    requires com.fasterxml.jackson.databind;

    exports com.example.order.api;
    opens com.example.order.model to com.fasterxml.jackson.databind;
}

常见错误:

错误 原因
package is not visible 未 exports
module does not read package 未 requires
reflection denied 未 opens

Spring Boot 应用通常以非模块化路径运行,但依赖库可能带有 module-info,具体由启动方式和模块系统决定。

本章小结

类加载分为加载、链接和初始化,双亲委派保证核心类库一致,SPI 和容器隔离需要打破默认委派。排查类问题要确认类来自哪个 jar、由哪个加载器加载、是否版本冲突、是否初始化失败。自定义加载器和热部署要特别关注 Metaspace 和 ClassLoader 泄漏。

思考题

  1. 准备阶段和初始化阶段分别做什么?
  2. 双亲委派解决什么问题?
  3. ClassNotFoundExceptionNoClassDefFoundError 有什么区别?
  4. 如何定位同名类来自哪个 jar?
  5. 为什么热部署可能造成 Metaspace 增长?