JVM类加载机制与双亲委派使用及说明
一、JVM 四大类加载器
JVM 的类加载器(ClassLoader)负责将.class字节码文件加载到内存中,并生成对应的Class对象。
Java 默认提供了四层层级结构的类加载器,从上到下依次为:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。
1. 层级结构与核心特性
| 类加载器名称 | 英文 / 简称 | 加载范围 | 实现方式 | 父加载器 |
|---|---|---|---|---|
| 启动类加载器 | Bootstrap ClassLoader | JRE 核心类库:rt.jar、resources.jar、sun.boot.class.path路径下的类(如java.lang.*、java.util.*) | C/C++ 实现,JVM 内置,无对应 Java 对象,无法在代码中直接获取 | 无(顶层加载器) |
扩展类加载器 (jdk9+后改名为平台类加载器) | Extension ClassLoader | JRE 扩展目录jre/lib/ext、java.ext.dirs指定路径下的扩展类 | Java 实现,继承自URLClassLoader | 启动类加载器 |
| 应用程序类加载器 | Application ClassLoader(系统类加载器) | 项目classpath下的自定义类、第三方依赖包 | Java 实现,继承自URLClassLoader,是ClassLoader.getSystemClassLoader()的返回值 | 扩展类加载器 |
| 自定义类加载器 | Custom ClassLoader | 自定义路径的类(网络、加密字节码、自定义目录等) | 继承ClassLoader重写核心方法实现 | 默认为应用程序类加载器 |
补充关键说明
- 层级关系≠继承关系:类加载器的父子关系是组合引用(通过
parent字段关联),不是 Java 类的继承; - 获取系统类加载器:代码中调用
ClassLoader.getSystemClassLoader(),默认获取的就是应用程序类加载器; - Bootstrap 特殊点:它是 JVM 本地代码实现的,在 Java 中获取它会返回
null。
2. 代码验证类加载器
通过简单代码查看不同类的加载器,直观理解层级关系:
public class ClassLoaderDemo {
public static void main(String[] args) {
// 1. 核心类:由Bootstrap加载,输出null
ClassLoader bootstrapLoader = String.class.getClassLoader();
System.out.println("String类加载器: " + bootstrapLoader);
// 2. 系统类加载器(应用类加载器)
ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("自定义类加载器: " + appLoader);
// 3. 应用类加载器的父加载器:扩展类加载器
ClassLoader extLoader = appLoader.getParent();
System.out.println("应用类加载器的父加载器: " + extLoader);
// 4. 扩展类加载器的父加载器:Bootstrap,输出null
System.out.println("扩展类加载器的父加载器: " + extLoader.getParent());
}
}
二、双亲委派机制
1. 定义
双亲委派机制是 JVM 类加载的默认规则:当一个类加载器收到类加载请求时,不会自己先尝试加载,而是将请求向上委托给父加载器,递归向上直到顶层启动类加载器;只有当父加载器无法加载该类时,子加载器才会尝试自己加载。
2. 执行流程(递归逻辑)
自定义类加载器收到加载请求,先检查是否已加载该类,已加载则直接返回Class对象;
未加载则委托给父加载器(应用类加载器);
应用类加载器重复检查逻辑,委托给扩展类加载器;
扩展类加载器重复检查逻辑,委托给启动类加载器;
启动类加载器尝试在自身加载路径查找类:
- 找到:直接加载并返回
Class对象; - 未找到:向下回溯,让子加载器尝试加载;
若所有层级加载器都无法加载,抛出ClassNotFoundException。
3. 核心优势
避免类重复加载:保证同一个类在 JVM 中只被加载一次,生成唯一的Class对象;
保障 Java 核心 API 安全(沙箱安全):防止恶意代码自定义核心类(如java.lang.String)替换 JDK 原生类,避免核心 API 被篡改;
层级管理类资源:规范不同范围类的加载边界,提升类加载的稳定性。
4. 源码层面解析
双亲委派的核心逻辑封装在ClassLoader的loadClass(String name, boolean resolve)方法中(JDK8 源码核心逻辑):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查类是否已经被当前加载器加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 递归委托父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 无父加载器,直接使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载,捕获异常
}
// 4. 父加载器加载失败,当前加载器自行加载
if (c == null) {
c = findClass(name);
}
}
// 解析类
if (resolve) {
resolveClass(c);
}
return c;
}
}
关键方法:findClass()是子类的扩展点,默认抛出异常,自定义加载器需要重写它。
三、打破双亲委派机制
1. 为什么要打破?
默认的双亲委派无法满足所有场景,常见需求:
- 加载自定义路径 / 特殊来源的类(网络字节码、加密字节码、模块化 jar);
- 实现类隔离(如 Web 容器 Tomcat、SPI 机制、热部署);
- 同一个应用中需要加载同一个类的不同版本。
2. 打破的核心原理
双亲委派的逻辑写在loadClass()方法中,重写loadClass()方法,跳过向上委托的逻辑,就能直接打破该机制。
3. 标准实现方式:自定义类加载器
步骤
- 继承
java.lang.ClassLoader抽象类; - 重写
loadClass()方法,破坏默认的双亲委派递归逻辑; - 重写
findClass()方法,实现自定义的类字节码读取逻辑。
完整示例代码
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 打破双亲委派的自定义类加载器
*/
public class CustomClassLoader extends ClassLoader {
// 自定义类加载路径
private final String classPath;
public CustomClassLoader(String classPath) {
// 关键:不指定父加载器(默认父加载器为系统类加载器,也可手动设置)
this.classPath = classPath;
}
/**
* 重写loadClass,核心:跳过双亲委派的向上委托逻辑
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 2. 打破规则:核心类仍交给Bootstrap加载,避免JDK核心类加载异常
if (name.startsWith("java.")) {
return getSystemClassLoader().loadClass(name);
}
// 3. 不委托父加载器,直接自己加载
try {
clazz = findClass(name);
} catch (Exception e) {
// 加载失败,降级为系统类加载器加载
return super.loadClass(name, resolve);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
/**
* 重写findClass:从自定义路径读取字节码
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassBytes(name);
if (classData == null) {
throw new ClassNotFoundException("无法找到类:" + name);
}
// 将字节码转换为Class对象
return defineClass(name, classData, 0, classData.length);
}
/**
* 从文件系统读取.class字节码
*/
private byte[] getClassBytes(String className) {
String path = classPath + "/" + className.replace(".", "/") + ".class";
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
return null;
}
}
}
测试代码
public class TestCustomLoader {
public static void main(String[] args) throws Exception {
// 自定义加载路径
CustomClassLoader loader = new CustomClassLoader("D:/test/classes");
// 加载自定义类
Class<?> clazz = loader.loadClass("com.test.DemoClass");
// 打印加载器,验证为自定义加载器
System.out.println("类加载器: " + clazz.getClassLoader());
}
}
4. 行业中打破双亲委派的经典场景
场景 1:Tomcat 等 Web 容器
- 需求:一个 Web 服务器部署多个应用,不同应用可能依赖同一个类的不同版本,需要类隔离;
- 实现:每个 Web 应用对应一个独立的
WebAppClassLoader,优先加载应用自身的类,跳过双亲委派向上委托逻辑,避免类冲突。
场景 2:JDBC SPI 服务发现
- JDK 的
rt.jar中定义了java.sql.Driver接口(由 Bootstrap 加载),但具体驱动实现类(MySQL/PostgreSQL 驱动)在classpath下; - Bootstrap 加载器无法加载应用层的驱动类,因此通过
Thread.getContextClassLoader()(线程上下文类加载器)打破委派,让启动类加载器反向使用应用类加载器加载实现类。
场景 3:热部署 / 热加载
- 如 JRebel、Spring Boot DevTools,通过自定义类加载器重新加载修改后的类,绕过默认的缓存机制,实现代码热更新。
总结
核心知识点回顾:
- 四大类加载器:Bootstrap(顶层、C 实现)→ Extension → Application → 自定义加载器,层级为组合关系而非继承;
- 双亲委派机制:向上委托、向下加载,核心价值是防重复加载、保障核心 API 安全,逻辑在
ClassLoader.loadClass()中; - 打破方式:重写
loadClass()方法跳过向上委托,配合自定义findClass()实现个性化加载; - 工业级场景:Tomcat 类隔离、JDBC SPI、热部署框架是打破双亲委派的典型应用。
面试答题话术参考:
- JVM 有四层类加载器,从顶层到底层分别是启动类、扩展类、应用类和自定义类加载器。
- 双亲委派是类加载的默认规则,请求会递归向上委托给父加载器,父加载器无法加载时子加载器才会处理,主要作用是避免类重复和保证核心类安全。
- 打破该机制的核心方法是自定义 ClassLoader 并重写 loadClass 方法,跳过委托逻辑,像 Tomcat 的类隔离、JDBC 的 SPI 机制都是实际业务中打破该机制的经典场景。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
SpringBoot利用jasypt实现配置文件加密的完整指南
在实际开发中,若出于安全考虑不想暴露一些敏感的配置,如数据库密码等,就需要对配置文件进行加密,下面我们来看看如何使用jasypt给配置文件加密吧2025-03-03


最新评论