类加载机制与双亲委派模型
类加载机制是 Java 虚拟机把 Class 文件加载到内存,并对其进行校验、转换解析和初始化的过程。理解这一机制对解决 ClassNotFoundException、NoClassDefFoundError 等问题至关重要。
类加载的时机
类在以下情况下会被加载:
- new、getstatic、putstatic、invokestatic 指令
- 反射调用
- 子类加载时父类先加载
- 主类(main 方法所在类)
- 动态语言支持(MethodHandle)
- 接口 default 方法
类加载的五个阶段
加载 -> 验证 -> 准备 -> 解析 -> 初始化
|
1. 加载(Loading)
任务:
- 通过全限定名获取二进制字节流
- 将字节流转化为方法区的运行时数据结构
- 生成 java.lang.Class 对象
来源:
- 本地文件系统(.class)
- 网络(Applet)
- ZIP/JAR(classpath)
- 动态生成(动态代理)
- 数据库
2. 验证(Verification)
确保 Class 文件的字节流中包含的信息符合 JVM 规范。
├─ 文件格式验证(魔数、版本号) ├─ 元数据验证(是否有父类、是否继承final类) ├─ 字节码验证(类型转换是否合法) ├─ 符号引用验证(是否能找到对应类)
|
3. 准备(Preparation)
为类变量(static)分配内存并设置初始值。
public class Sample { private static int value = 123; private static final int 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; static { b = 2; } private static int b = 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();
|
双亲委派模型
工作流程
类加载请求 | 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)) { Class<?> c = findLoadedClass(name); if (c == null) { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } if (c == null) { c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
|
双亲委派的好处
- 避免类的重复加载:先检查是否已加载
- 保证扩展性:父加载器加载的类对子加载器可见
- 保证安全性:核心类(String、Object)由 Bootstrap 加载,防止篡改
package java.lang;
public class String { }
|
打破双亲委派
1. 线程上下文类加载器
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
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 应用创建独立的类加载器,实现应用隔离和热部署。
自定义类加载器
应用场景
- 热部署:不重启 JVM 更新类
- 类隔离:不同版本共存
- 加密保护:解密后加载
- 从网络/数据库加载
简单实现
public class CustomClassLoader extends ClassLoader { private String classPath; public CustomClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); return defineClass(name, classData, 0, classData.length); } private byte[] loadClassData(String name) { 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 平台无关性和安全性的基石,也是实现模块化、热部署等技术的基础。