类加载器分类
JVM 类加载器分类与双亲委派机制
类加载器(ClassLoader)是 JVM 实现类加载机制的核心组件,负责将不同来源的 .class 文件加载到内存中。JVM 中类加载器分为默认类加载器和自定义类加载器,它们通过 “双亲委派机制” 协同工作,确保类加载的安全性和一致性。本文将详细解析各类加载器的特点、职责,以及双亲委派机制的原理与应用。
默认类加载器:JVM 内置的加载器
JVM 提供了三种默认类加载器,各自负责特定路径的类加载,形成层级关系(非继承,而是委托关系)。
引导类加载器(Bootstrap ClassLoader)
- 实现语言:由 C/C++ 编写(非 Java 代码),是 JVM 自身的一部分。
- 加载范围:负责加载 JDK 核心类库,具体包括:
JAVA_HOME/jre/lib目录下的核心 jar 包(如rt.jar、resources.jar等,名称符合 JVM 识别规则的类库);- 由系统属性
sun.boot.class.path指定的路径。
- 特点:
- 没有继承
java.lang.ClassLoader类(是 JVM 内置组件); - 是所有类加载器的 “根加载器”,其他加载器的 “父加载器” 默认指向它;
- 无法通过 Java 代码直接获取其实例(
Class.getClassLoader()对核心类返回null,如Object.class.getClassLoader()返回null)。
- 没有继承
扩展类加载器(Extension ClassLoader)
- 实现类:
sun.misc.Launcher$ExtClassLoader(Java 语言编写),继承java.lang.ClassLoader。 - 加载范围:负责加载 Java 扩展类库,具体包括:
JAVA_HOME/jre/lib/ext目录下的 jar 包;- 由系统属性
java.ext.dirs指定的路径。
- 父加载器:逻辑上的父加载器是引导类加载器(实际通过委托机制关联,而非继承)。
- 作用:扩展 JDK 功能,用户可将自定义扩展类放入
ext目录,由其自动加载。
系统类加载器(Application ClassLoader)
- 实现类:
sun.misc.Launcher$AppClassLoader(Java 语言编写),继承java.lang.ClassLoader。 - 加载范围:负责加载应用程序的类,具体包括:
- 由
classpath环境变量指定的路径(可通过-cp或-classpath命令行参数修改); - JAR 包中
Manifest文件的Class-Path属性指定的路径。
- 由
- 父加载器:扩展类加载器。
- 特点:
- 是程序默认的类加载器,应用中大多数类由它加载;
- 可通过
ClassLoader.getSystemClassLoader()方法获取其实例。
示例:验证类加载器的加载路径
public class ClassLoaderDemo {
public static void main(String[] args) {
// 核心类(如Object)由引导类加载器加载(返回null)
System.out.println(Object.class.getClassLoader()); // null
// 扩展类(如javax.swing.JFrame)由扩展类加载器加载
System.out.println(javax.swing.JFrame.class.getClassLoader());
// 输出:sun.misc.Launcher$ExtClassLoader@xxxx
// 应用类由系统类加载器加载
System.out.println(ClassLoaderDemo.class.getClassLoader());
// 输出:sun.misc.Launcher$AppClassLoader@xxxx
}
}
自定义类加载器:灵活扩展加载逻辑
除默认加载器外,开发者可通过继承 java.lang.ClassLoader 实现自定义类加载器,满足特殊需求(如加载加密类、从网络加载类、实现应用隔离等)。
自定义类加载器的核心方法
自定义类加载器需重写以下关键方法(遵循双亲委派机制):
| 方法 | 作用 | 是否建议重写 |
|---|---|---|
findClass(String name) |
在父加载器加载失败后,根据自定义逻辑查找并加载类(如从特定路径读取字节码) | 是(核心方法,保证遵循双亲委派) |
defineClass(String name, byte[] b, int off, int len) |
将字节数组(byte[])解析为 JVM 可识别的 Class 对象(不可手动修改,由父类提供) |
否(直接调用父类实现) |
loadClass(String name) |
实现双亲委派的核心逻辑(先委托父加载器,失败后调用 findClass) |
否(除非需要打破双亲委派) |
resolveClass(Class<?> c) |
链接类(验证、准备、解析),确保类可执行 | 按需调用(默认不自动链接) |
自定义类加载器示例
以下是一个简单的自定义类加载器,从指定目录加载类:
public class CustomClassLoader extends ClassLoader {
private String classPath; // 类加载路径
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
// 重写findClass:自定义类查找逻辑
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 将类名转换为文件路径(如com.example.User → com/example/User.class)
String path = name.replace(".", "/") + ".class";
// 从指定路径读取字节码
byte[] classData = loadClassData(path);
// 将字节码解析为Class对象
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("类加载失败:" + name, e);
}
}
// 读取类文件的字节数据
private byte[] loadClassData(String path) throws IOException {
try (InputStream is = new FileInputStream(classPath + "/" + path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
public static void main(String[] args) throws Exception {
// 创建自定义类加载器(加载路径为./custom/classes)
CustomClassLoader loader = new CustomClassLoader("./custom/classes");
// 加载com.example.Test类
Class<?> clazz = loader.loadClass("com.example.Test");
// 反射执行方法
clazz.getMethod("sayHello").invoke(clazz.newInstance());
}
}
说明:
- 自定义加载器通过
findClass实现从./custom/classes目录加载类,父加载器默认是系统类加载器; - 加载流程遵循双亲委派:先委托父加载器(系统类加载器)加载,若父加载器未找到,再调用
findClass加载。
双亲委派机制:类加载的安全保障
双亲委派机制是类加载器之间的协作规则,核心是 “先委托父加载器加载,父加载器无法加载时再自己尝试”,确保类加载的一致性和安全性。
核心原理
- 委托父加载:当一个类加载器收到加载请求时,首先将请求委托给其父加载器(而非自己直接加载)。
- 递归向上:父加载器重复委托过程,直到请求到达顶层的引导类加载器。
- 尝试加载:若父加载器能加载该类,则返回加载结果;若所有父加载器都无法加载,当前加载器才会尝试自己加载(调用
findClass方法)。
流程图:

源码体现(ClassLoader.loadClass 方法)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 若有父加载器,委托父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 无父加载器(如扩展类加载器),委托引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载
}
if (c == null) {
// 4. 父加载器均无法加载,调用findClass自己加载
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c); // 链接类
}
return c;
}
}
核心作用
- 避免类重复加载:同一类(全限定名相同)由同一个加载器加载,确保 JVM 中类的唯一性(类的唯一性由 “加载器 + 全限定名” 共同决定)。
- 保护核心类安全:防止恶意类篡改 JDK 核心类(如自定义
java.lang.Object类,会被引导类加载器优先加载核心Object,避免替换)。 - 层级协作:父加载器加载的类可被所有子加载器共享(如核心类
String由引导类加载器加载,所有应用都可使用)。
局限性与打破双亲委派的场景
双亲委派机制的单向委托(子→父)导致顶层加载器无法访问底层加载器加载的类,某些场景下需要打破这一机制:
- Web 容器隔离:如 Tomcat 需为每个 Web 应用创建独立类加载器,避免不同应用的类冲突(如同一类的不同版本)。Tomcat 采用 “当前类加载器优先” 策略,先尝试自己加载,再委托父加载器。
- 热部署:通过自定义类加载器重新加载类(旧类加载器被回收,新类加载器加载更新后的类)。
- SPI 机制:Java 的 SPI(如 JDBC)中,核心类(如
DriverManager)由引导类加载器加载,但需调用用户实现的驱动类(由系统类加载器加载),通过线程上下文类加载器(Thread.getContextClassLoader())打破委派限制。
Tomcat 类加载器结构(打破双亲委派的典型案例)
Tomcat 为实现 Web 应用隔离,自定义了多层类加载器:
- Bootstrap:引导类加载器(加载 JVM 核心类)。
- System:系统类加载器(加载 Tomcat 自身启动类)。
- Common:通用类加载器(加载 Tomcat 和所有应用共享的类,如
servlet-api.jar)。 - WebApp:应用类加载器(每个 Web 应用一个,加载
WEB-INF/classes和WEB-INF/lib下的类,优先自己加载,再委托父加载器)。
类加载器的其他关键细节
类加载器的 “父子关系”
- 类加载器的 “父加载器” 通过
ClassLoader类的parent字段维护(组合关系),而非继承关系。 - 可通过
ClassLoader.getParent()方法获取父加载器(引导类加载器的父加载器为null)。
数组类的加载器
- 数组类的类加载器与数组元素的类加载器一致(如
User[]的加载器与User的加载器相同)。 - 基本类型数组(如
int[])没有类加载器(int[].class.getClassLoader()返回null)。
类的相等性判断
两个类相等(Class.equals() 返回 true)的前提是:
- 全限定名相同;
- 由同一个类加载器加载。
即使两个类的字节码完全相同,若由不同加载器加载,也会被视为不同的类