Java类加载机制与双亲委派模型
类加载机制将.class文件加载到JVM内存,是Java运行的基础。
类加载过程
Java
┌────────────┐
│ 加载 │ ← 查找并加载字节码
└────────────┘
↓
┌────────────┐
│ 链接 │
├────────────┤
│ 验证 │ ← 验证字节码合法性
│ 准备 │ ← 分配内存,初始化静态变量
│ 解析 │ ← 符号引用转为直接引用
└────────────┘
↓
┌────────────┐
│ 初始化 │ ← 执行类初始化代码
└────────────┘
详细步骤
1. 加载
- 通过类名获取二进制字节流
- 转换为方法区的运行时数据结构
- 在堆中生成Class对象
2. 验证
- 文件格式验证:魔数0xCAFEBABE
- 元数据验证:语义分析
- 字节码验证:数据流分析
- 符号引用验证:引用是否存在
3. 准备
Java
public class Example {
private static int value = 100; // 准备阶段:value=0
private static final int MAX = 100; // 准备阶段:MAX=100
}
静态变量在准备阶段初始化为零值,final变量直接赋值
4. 解析
将符号引用转为直接引用:
Java
符号引用:字符串形式的引用
"java/lang/Object"
直接引用:内存地址
0x12345678
5. 初始化
执行类初始化代码<clinit>方法:
Java
public class Example {
private static int value = 100; // <clinit>执行赋值
private static int count;
static {
count = 10; // <clinit>执行静态块
}
}
类加载器层次
Java
┌─────────────────────────────────────┐
│ Bootstrap ClassLoader │ ← JVM内置,加载核心类
│ (rt.jar, java.*包) │
└─────────────────────────────────────┘
↓ parent
┌─────────────────────────────────────┐
│ Extension ClassLoader │ ← 加载扩展类
│ (ext目录下的jar) │
└─────────────────────────────────────┘
↓ parent
┌─────────────────────────────────────┐
│ Application ClassLoader │ ← 加载应用类
│ (classpath下的类) │
└─────────────────────────────────────┘
↓ parent
┌─────────────────────────────────────┐
│ Custom ClassLoader │ ← 自定义加载器
└─────────────────────────────────────┘
双亲委派模型
工作流程
Java
收到加载请求
↓
委派给父加载器
↓
父加载器无法加载 → 尔回子加载器
↓
子加载器尝试加载
实现代码
Java
protected Class<?> loadClass(String name, boolean resolve) {
// 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;
}
双亲委派优点
- 安全性:防止核心类被篡改
Java
// 自定义java.lang.String会失败
// Bootstrap已加载,不会用自定义的
- 避免重复加载:父加载器已加载的类,子加载器不会再加载
打破双亲委派
方式1:自定义加载器覆写loadClass
text
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) {
// 直接加载,不委派父加载器
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name); // 自己加载
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
方式2:线程上下文加载器
text
// JDBC驱动加载示例
Thread.currentThread().setContextClassLoader(driverLoader);
// SPI框架使用上下文加载器加载实现类
// 让父加载器加载接口,子加载器加载实现
方式3:OSGi热部署
每个Bundle有独立类加载器,打破双亲委派实现模块隔离。
自定义类加载器
text
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
private byte[] loadClassData(String name) {
String path = classPath + name.replace('.', '/') + ".class";
try {
InputStream in = new FileInputStream(path);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 使用
MyClassLoader loader = new MyClassLoader("/custom/path/");
Class<?> clazz = loader.loadClass("com.example.CustomClass");
类加载时机
| 触发条件 | 说明 |
|---|---|
| new对象 | 创建实例 |
| 访问静态字段 | getstatic/putstatic |
| 调用静态方法 | invokestatic |
| 反射 | Class.forName() |
| 初始化子类 | 父类先初始化 |
| 主类 | 包含main方法的类 |
被动引用不触发初始化:
text
public class Parent {
static { System.out.println("Parent初始化"); }
public static int value = 100;
}
// 子类引用父类静态字段,不初始化子类
public class Child extends Parent {
static { System.out.println("Child初始化"); }
}
Child.value; // 只初始化Parent,不初始化Child
注意事项
双亲委派保证核心类安全,不应轻易打破
自定义加载器建议覆写findClass而非loadClass
线程上下文加载器用于SPI场景
类初始化是懒加载,不会加载所有类
要点总结
- 类加载过程:加载 → 验证 → 准备 → 解析 → 初始化
- 类加载器层次:Bootstrap → Extension → Application → Custom
- 双亲委派:先委派父加载器,保证安全性和唯一性
- 打破双亲委派:覆写loadClass、上下文加载器、OSGi
- 自定义加载器覆写findClass方法
📝 发现内容有误?点击此处直接编辑