目录
1. 启动类加载器(Bootstrap ClassLoader)
2. 扩展类加载器(Extension ClassLoader)
3. 应用程序类加载器(Application ClassLoader)
4. 自定义类加载器(Custom ClassLoader)
什么是双亲委派机制
双亲委派机制(Parents Delegation Model)是JVM中类加载器的一种工作机制。当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父类加载器无法完成加载请求时,子类加载器才会尝试自己去加载。
这种机制确保了Java核心API的类不会被随意替换,维护了Java运行环境的安全性和稳定性。
类加载器的层级结构
Java中的类加载器按照层级关系分为以下几种:
1. 启动类加载器(Bootstrap ClassLoader)
- 位置:JVM内部实现,由C++代码实现
- 作用:加载Java核心类库(如
java.lang.*
、java.util.*
等) - 路径:
$JAVA_HOME/lib
目录下的类库 - 特点:最顶层的类加载器,没有父类加载器
2. 扩展类加载器(Extension ClassLoader)
- 位置:
sun.misc.Launcher$ExtClassLoader
- 作用:加载扩展类库
- 路径:
$JAVA_HOME/lib/ext
目录下的类库 - 父类加载器:启动类加载器
3. 应用程序类加载器(Application ClassLoader)
- 位置:
sun.misc.Launcher$AppClassLoader
- 作用:加载应用程序类路径(ClassPath)上的类
- 路径:环境变量ClassPath指定的路径
- 父类加载器:扩展类加载器
- 特点:也称为系统类加载器
4. 自定义类加载器(Custom ClassLoader)
- 作用:用户根据需要自定义的类加载器
- 父类加载器:通常是应用程序类加载器
// 查看类加载器层级结构的示例代码
public class ClassLoaderHierarchy {
public static void main(String[] args) {
// 获取当前类的类加载器
ClassLoader classLoader = ClassLoaderHierarchy.class.getClassLoader();
System.out.println("当前类的类加载器:" + classLoader);
// 获取父类加载器
ClassLoader parentClassLoader = classLoader.getParent();
System.out.println("父类加载器:" + parentClassLoader);
// 获取祖父类加载器
ClassLoader grandParentClassLoader = parentClassLoader.getParent();
System.out.println("祖父类加载器:" + grandParentClassLoader);
// 输出结果:
// 当前类的类加载器:sun.misc.Launcher$AppClassLoader@2a139a55
// 父类加载器:sun.misc.Launcher$ExtClassLoader@15db9742
// 祖父类加载器:null (Bootstrap ClassLoader由C++实现,在Java中显示为null)
}
}
双亲委派机制的工作原理
双亲委派机制的工作流程如下:
- 接收加载请求:类加载器接收到类加载请求
- 向上委派:不立即加载,而是委派给父类加载器
- 递归委派:父类加载器继续向上委派,直到启动类加载器
- 尝试加载:启动类加载器尝试加载类
- 向下返回:如果加载失败,返回给子类加载器尝试加载
- 最终加载:直到某个类加载器成功加载类或全部失败
graph TD
A[自定义类加载器] --> B[应用程序类加载器]
B --> C[扩展类加载器]
C --> D[启动类加载器]
D --> E{能否加载?}
E -->|能| F[加载完成]
E -->|不能| G[委派给子类加载器]
G --> H{扩展类加载器能否加载?}
H -->|能| I[加载完成]
H -->|不能| J[委派给子类加载器]
J --> K{应用程序类加载器能否加载?}
K -->|能| L[加载完成]
K -->|不能| M[委派给子类加载器]
M --> N{自定义类加载器能否加载?}
N -->|能| O[加载完成]
N -->|不能| P[抛出ClassNotFoundException]
为什么需要双亲委派机制
1. 避免类的重复加载
如果没有双亲委派机制,每个类加载器都可能加载同一个类,导致内存中存在多个相同的类对象。
2. 保证Java核心API的安全性
防止核心API被恶意替换。例如,如果有人自定义了一个java.lang.String
类,通过双亲委派机制,最终会由启动类加载器加载JDK中的String
类,而不是用户自定义的类。
3. 保证类的唯一性
在JVM中,类的唯一性是由类加载器和类的全限定名共同决定的。双亲委派机制确保了同一个类只会被同一个类加载器加载一次。
双亲委派机制的源码分析
让我们来看看ClassLoader
类中loadClass
方法的实现:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查该类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父类加载器,委派给父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有父类加载器,说明是启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 如果父类加载器无法加载,则调用自己的findClass方法进行加载
long t1 = System.nanoTime();
c = findClass(name);
// 记录加载时间统计
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制的破坏
虽然双亲委派机制很重要,但在某些场景下需要被破坏:
1. 自定义类加载器
通过重写loadClass
方法来改变类加载的行为:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先检查是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 对于自定义的类,直接由当前类加载器加载
if (name.startsWith("com.example.")) {
c = findClass(name);
} else {
// 其他类仍然遵循双亲委派
c = super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 实现自定义的类加载逻辑
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 从自定义位置加载类的字节码
// 这里可以从网络、数据库等位置加载
return null; // 简化示例
}
}
2. 线程上下文类加载器
在某些情况下,父类加载器需要加载由子类加载器加载的类,这时可以使用线程上下文类加载器:
public class ContextClassLoaderExample {
public static void main(String[] args) {
// 获取当前线程的上下文类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("上下文类加载器:" + contextClassLoader);
// 设置自定义的上下文类加载器
Thread.currentThread().setContextClassLoader(new CustomClassLoader());
// 在某些框架中,会使用上下文类加载器来加载类
// 例如:JDBC驱动加载、Spring容器等
}
}
3. 热替换和热部署
在开发环境中,为了实现热替换功能,需要破坏双亲委派机制:
public class HotSwapClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 对于需要热替换的类,每次都重新加载
if (isHotSwapClass(name)) {
c = findClass(name);
} else {
c = super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
private boolean isHotSwapClass(String name) {
// 判断是否是需要热替换的类
return name.startsWith("com.example.hotswap");
}
}
实际应用场景
1. Web应用服务器
Tomcat等Web服务器为了实现应用隔离,每个Web应用都有自己的类加载器:
// Tomcat的类加载器层级结构
// Bootstrap ClassLoader
// |
// System ClassLoader
// |
// Common ClassLoader
// |
// Catalina ClassLoader Shared ClassLoader
// |
// WebApp ClassLoader
2. OSGi框架
OSGi框架完全破坏了双亲委派机制,实现了网状的类加载器结构。
3. 模块化系统
Java 9的模块系统也对双亲委派机制进行了一定的改进。
总结
双亲委派机制是Java类加载器的核心机制,它具有以下特点:
优点:
- 避免类的重复加载
- 保证Java核心API的安全性
- 维护类的唯一性
缺点:
- 在某些场景下过于严格,需要被破坏
- 可能导致类加载的性能问题
适用场景:
- 大部分标准Java应用
- 需要保证类加载安全性的场景
破坏场景:
- 自定义类加载器
- 热替换和热部署
- 模块化系统
- Web应用服务器