类加载机制与双亲委派模型

类加载机制与双亲委派模型

类加载机制是 Java 虚拟机把 Class 文件加载到内存,并对其进行校验、转换解析和初始化的过程。理解这一机制对解决 ClassNotFoundException、NoClassDefFoundError 等问题至关重要。

类加载的时机

类在以下情况下会被加载:

  1. new、getstatic、putstatic、invokestatic 指令
  2. 反射调用
  3. 子类加载时父类先加载
  4. 主类(main 方法所在类)
  5. 动态语言支持(MethodHandle)
  6. 接口 default 方法

类加载的五个阶段

加载 -> 验证 -> 准备 -> 解析 -> 初始化

1. 加载(Loading)

任务

  1. 通过全限定名获取二进制字节流
  2. 将字节流转化为方法区的运行时数据结构
  3. 生成 java.lang.Class 对象

来源

  • 本地文件系统(.class)
  • 网络(Applet)
  • ZIP/JAR(classpath)
  • 动态生成(动态代理)
  • 数据库

2. 验证(Verification)

确保 Class 文件的字节流中包含的信息符合 JVM 规范。

├─ 文件格式验证(魔数、版本号)
├─ 元数据验证(是否有父类、是否继承final类)
├─ 字节码验证(类型转换是否合法)
├─ 符号引用验证(是否能找到对应类)

3. 准备(Preparation)

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

public class Sample {
private static int value = 123; // 准备阶段: value = 0
// 初始化阶段: value = 123

private static final int CONSTANT = 123; // 准备阶段: CONSTANT = 123
}

注意final static 常量在准备阶段就赋值为最终值。

4. 解析(Resolution)

将常量池中的符号引用替换为直接引用。

// 符号引用
String className = "java.lang.String";

// 直接引用
Class<?> clazz = Class.forName(className); // 内存地址

5. 初始化(Initialization)

执行 <clinit>() 方法(类构造器)。

public class InitOrder {
private static int a = 1; // (1)

static { // (2)
b = 2;
}

private static int b = 3; // (3) 最终b=3

// <clinit>() 执行顺序: (1) -> (2) -> (3)
}

注意

  • <clinit>() 由编译器自动收集所有 static 变量和 static 块
  • 子类的 <clinit>() 执行前,父类的 <clinit>() 先执行
  • 接口也有 <clinit>(),但实现类初始化时不会触发

类加载器

三层类加载器

┌─────────────────────────────────────┐
│ Bootstrap ClassLoader │ (C++实现,加载<JAVA_HOME>/lib)
├─────────────────────────────────────┤
│ Extension ClassLoader │ (加载<JAVA_HOME>/lib/ext)
├─────────────────────────────────────┤
│ Application ClassLoader │ (加载classpath)
├─────────────────────────────────────┤
│ User ClassLoader │ (自定义)
└─────────────────────────────────────┘

各加载器的职责

加载器 实现语言 加载路径 父加载器
Bootstrap C++ <JAVA_HOME>/lib
Extension Java <JAVA_HOME>/lib/ext Bootstrap
Application Java classpath Extension
// 获取类加载器
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
ClassLoader extLoader = appLoader.getParent();
ClassLoader bootstrapLoader = extLoader.getParent(); // null

双亲委派模型

工作流程

类加载请求
|
v
Custom ClassLoader
| 不处理,委派给父加载器
v
App ClassLoader
| 不处理,委派给父加载器
v
Ext ClassLoader
| 不处理,委派给父加载器
v
Bootstrap ClassLoader
| 尝试加载
├─ 成功 -> 返回Class对象
└─ 失败 -> 子加载器尝试
v
Ext ClassLoader -> App -> Custom

源码实现

protected Class<?> loadClass(String name, boolean resolve) 
throws ClassNotFoundException {

synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);

if (c == null) {
// 2. 委派父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}

// 3. 父加载器无法加载,自己加载
if (c == null) {
c = findClass(name);
}
}

if (resolve) {
resolveClass(c);
}
return c;
}
}

双亲委派的好处

  1. 避免类的重复加载:先检查是否已加载
  2. 保证扩展性:父加载器加载的类对子加载器可见
  3. 保证安全性:核心类(String、Object)由 Bootstrap 加载,防止篡改
// 如果没有双亲委派,可以自定义恶意String
package java.lang;

public class String {
// 恶意代码...
}

打破双亲委派

1. 线程上下文类加载器

// JDBC 例子:DriverManager 由 Bootstrap 加载
// 但具体驱动实现由 App ClassLoader 加载

// 获取线程上下文类加载器
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();

// ServiceLoader 使用上下文类加载器加载实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

原理:Bootstrap 加载的类可以通过 Thread.currentThread().getContextClassLoader() 委派给子加载器。

2. OSGi / 热部署

// 自定义类加载器,先自己加载,找不到再委派
public class HotSwapClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {

// 先自己尝试加载
Class<?> clazz = findClass(name);
if (clazz != null) {
return clazz;
}

// 找不到再委派
return super.loadClass(name, resolve);
}
}

3. Tomcat 的类加载器

Common ClassLoader
├── Catalina ClassLoader(Tomcat自身)
└── Shared ClassLoader
└── WebApp ClassLoader(Web应用,先自己加载)

Tomcat 为每个 Web 应用创建独立的类加载器,实现应用隔离和热部署。

自定义类加载器

应用场景

  1. 热部署:不重启 JVM 更新类
  2. 类隔离:不同版本共存
  3. 加密保护:解密后加载
  4. 从网络/数据库加载

简单实现

public class CustomClassLoader extends ClassLoader {
private String classPath;

public CustomClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 读取class文件
byte[] classData = loadClassData(name);

// 2. 定义类
return defineClass(name, classData, 0, classData.length);
}

private byte[] loadClassData(String name) {
// 从自定义路径加载class文件
String path = classPath + File.separatorChar
+ name.replace('.', File.separatorChar) + ".class";
try (InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length;
while ((length = is.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

常见异常

异常 原因 解决
ClassNotFoundException 类路径错误 检查 classpath
NoClassDefFoundError 编译时有,运行时缺失 检查依赖包
ClassCastException 不同类加载器加载同一类 统一类加载器
LinkageError 重复定义类 检查类加载逻辑

总结

概念 要点
加载过程 加载->验证->准备->解析->初始化
双亲委派 委派给父加载器,父无法加载才自己加载
好处 避免重复加载、保证安全性
打破场景 JDBC、OSGi、Tomcat、热部署
自定义 继承ClassLoader,重写findClass

类加载机制是 Java 平台无关性和安全性的基石,也是实现模块化、热部署等技术的基础。


   转载规则


《类加载机制与双亲委派模型》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录