1. 概述
双亲委派模型(Parent Delegation Model)是 Java 类加载机制中的一个核心概念,它用于组织和管理 Java 类加载器的工作方式。Java 虚拟机(JVM)通过类加载器(ClassLoader)将 .class
文件加载到内存中,并且通过双亲委派模型来保证类的加载过程具有一定的安全性和统一性。它的核心思想是:类加载器应该先委托它的父类加载器尝试加载类,只有当父类加载器无法加载时,才由当前类加载器自己加载该类。
双亲委派模型的设计主要是为了避免 Java 中的类加载混乱问题,确保了 Java 核心类库的安全和唯一性。
2. 类加载器的概述
在 Java 中,类加载器是将 Java 字节码文件(通常是 .class
文件)加载到 JVM 中的重要组件。JVM 提供了几种默认的类加载器,并允许用户自定义类加载器。Java 中常见的类加载器包括:
-
Bootstrap ClassLoader(启动类加载器)
- 负责加载 Java 核心库,如
rt.jar
中的类。 - 这个类加载器是由 JVM 实现的,用来加载 Java 标准库(如
java.lang
和java.util
包中的类)。 - 它不是 Java 类,而是由底层的 C++ 实现,无法在 Java 代码中直接引用。
- 负责加载 Java 核心库,如
-
Extension ClassLoader(扩展类加载器)
- 负责加载扩展类库,通常是位于
JAVA_HOME/lib/ext
目录中的 JAR 包。 - 它是由
sun.misc.Launcher$ExtClassLoader
实现的。
- 负责加载扩展类库,通常是位于
-
Application ClassLoader(系统类加载器)
- 负责加载应用程序的类路径(classpath)中的类。
- 由
sun.misc.Launcher$AppClassLoader
实现。 - 这是默认的类加载器,用于加载用户类和第三方库。
3. 双亲委派模型的工作原理
双亲委派模型的工作原理非常简单,类加载器按照以下步骤加载类:
- 类加载器收到加载请求时,首先不会自己加载类,而是将这个请求委派给它的父类加载器去处理。
- 父类加载器继续向上委派,直到 Bootstrap ClassLoader。
- Bootstrap ClassLoader 尝试加载类。如果该类属于 Java 核心类库(如
java.lang.String
),则由它加载成功;否则,加载失败。 - 如果 Bootstrap ClassLoader 不能加载这个类,它会返回请求给下层的类加载器,依次返回到应用类加载器。
- 当前类加载器最终尝试加载,如果它也无法加载,抛出
ClassNotFoundException
。
这种逐级委派机制保证了 Java 类的唯一性。例如,系统中只有一个 java.lang.String
类,由 Bootstrap ClassLoader 统一加载,避免了核心类库被重复加载或覆盖。
3.1 类加载的层次结构
通过双亲委派模型,不同的类加载器形成了一种树形层次结构,类加载请求总是从当前加载器逐级向上委派到 Bootstrap ClassLoader。以下是常见的类加载器层次结构:
Bootstrap ClassLoader
|
Extension ClassLoader
|
Application ClassLoader (System ClassLoader)
- Bootstrap ClassLoader 位于最顶层,加载 Java 标准库。
- Extension ClassLoader 位于中间层,加载扩展库。
- Application ClassLoader 处于底层,负责加载用户类。
3.2 示例
假设有一个 Java 类 TestClass
位于应用程序类路径中。当程序请求加载 TestClass
时,加载流程如下:
- Application ClassLoader 收到加载
TestClass
的请求。 - 它首先将请求委派给 Extension ClassLoader。
- Extension ClassLoader 无法加载这个类,再将请求委派给 Bootstrap ClassLoader。
- Bootstrap ClassLoader 也找不到该类,于是将请求返回给 Extension ClassLoader。
- 最终,Application ClassLoader 自己尝试加载
TestClass
并成功。
通过这种机制,JVM 确保了核心类库优先由父类加载器加载,避免了用户代码覆盖系统核心类的情况。
4. 双亲委派模型的优势
4.1 类的唯一性
双亲委派模型确保了核心类库的唯一性。例如,java.lang.Object
类在整个 Java 虚拟机中只有一个副本,这样可以避免用户代码或恶意代码通过自定义类加载器替换或污染核心类库。
4.2 安全性
双亲委派模型通过委派父类加载器加载系统类,防止用户或第三方库加载自定义的类替换系统核心类,增强了系统的安全性。例如,恶意程序无法加载自己的 java.lang.String
类来篡改 JVM 行为。
4.3 高效的类加载
委派机制允许类加载器复用父类加载器已经加载的类,从而减少重复加载的开销。当某个类已经由父类加载器加载后,子类加载器不需要重新加载该类。
5. 打破双亲委派机制
尽管双亲委派模型提供了良好的类加载机制,但在某些情况下,我们可能需要打破双亲委派规则。例如,Java 服务器容器(如 Tomcat)在实现热部署和隔离不同应用程序的类加载时,通常会自定义类加载器并改变委派规则。
5.1 自定义类加载器
可以通过继承 ClassLoader
并重写 loadClass()
方法来自定义类加载器,打破双亲委派模型。例如,以下是一个简单的自定义类加载器示例:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义类加载逻辑
// 例如:从特定路径加载类
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String className) {
// 实现从文件或网络加载字节码的逻辑
return null;
}
}
在这个例子中,我们通过 findClass()
方法实现类的加载,而不是委派给父类加载器。
5.2 应用场景
-
Java EE 容器:如 Tomcat、WebLogic 等 Java EE 容器需要隔离不同 Web 应用的类加载,因此它们往往自定义类加载器来打破双亲委派模型,确保每个应用都可以加载自己的类版本而不冲突。
-
OSGi 框架:OSGi 允许动态加载和卸载模块,它通过打破双亲委派模型来实现模块化加载机制,使每个模块都有自己的类加载器。
6. 类加载的几个阶段
在 JVM 中,类加载是一个复杂的过程,主要分为以下几个阶段:
- 加载(Loading):通过类加载器查找
.class
文件,并读取其字节码,将其加载到内存中。 - 验证(Verification):确保类的字节码符合 JVM 规范,保证类文件的正确性和安全性。
- 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值(如
0
或null
)。 - 解析(Resolution):将类、接口和字段的符号引用替换为直接引用。
- 初始化(Initialization):执行类的静态代码块(
static
代码块)和静态变量的初始化。
7. 双亲委派模型的缺点
虽然双亲委派模型有很多优点,但在某些特殊情况下,它也会带来一些问题或局限性:
7.1 自定义加载器难以加载系统类
如果开发者希望自定义类加载器加载系统类(如 java.lang.String
),双亲委派机制会阻止这种行为,因为系统类首先由父类加载器加载。
7.2 无法隔离应用
双亲委派模型无法实现类的隔离加载。例如,在 Web 容器中,如果两个 Web 应用需要加载不同版本的库,默认的双亲委派机制无法满足这个需求,因此必须使用自定义类加载器来打破这个机制。
8. 总结
双亲委派模型是 Java 类加载机制的核心思想之一,它确保了 Java 类加载过程的安全性和一致性。通过委派机制,父类加载器优先加载类,避免了类的重复加载和核心类库的篡改。
尽管双亲委派模型有许多优点,但在一些复杂场景中,特别是需要类加载隔离或热部署的场景下,需要自定义类加载器并打破双亲委派规则。