Java 入门指南:JVM(Java虚拟机)—— 双亲委派模型(Parent Delegation Model)
文章目录
- 双亲委派模型
- 执行流程
- 示例流程
- 双亲委派模型的优势
- 打破双亲委派模型
- 总结
双亲委派模型
双亲委派模型(Parent Delegation Model)是 Java 类加载机制中的一个核心概念,它用于组织和管理 Java 类加载器的工作方式。在Java中,类加载器负责将 .class
文件加载到内存中,并通过双亲委派模型来保证类的加载过程具有一定的安全性和统一性。
这一模型的核心思想是:类加载器在接收到类加载请求时,首先不会自己尝试加载该类,而是将这个请求委派给它的父类加载器去处理。只有当父类加载器无法加载该类时,当前类加载器才会尝试自己去加载。
根据官方的说明:
ClassLoader
类使用委托模型来搜索类和资源。每个 ClassLoader
实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 “bootstrap class loader
” 的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader
实例的父类加载器。
这里的双亲更多地表达的是“父母这一辈”的人,并不是说真的有一个 MotherClassLoader
和一个 FatherClassLoader
。
另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。
public abstract class ClassLoader {
...
// 组合
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
...
}
执行流程
双亲委派模型的实现代码的逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,以下是 loadClass()
的源代码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
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) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
-
检查类是否已被加载:当一个类加载器接收到类加载请求时,它首先会检查该类是否已经被加载过。如果已经加载过,就直接返回该类的
Class
对象,避免重复加载。 -
委派给父类加载器:如果该类尚未被加载,类加载器就会将加载请求委派给它的父类加载器。这个过程会一直进行,直到达到顶层的启动类加载器(Bootstrap ClassLoader)。
-
父类加载器尝试加载:父类加载器在接收到加载请求后,会尝试在自己的搜索范围内加载该类。如果找到了相应的类文件,就加载并返回该类的
Class
对象;如果没有找到,就将请求继续向上委派。 -
子类加载器尝试加载:如果所有的父类加载器都无法加载该类,那么最初的类加载器就会尝试自己去加载该类。如果它也无法加载,就会抛出
ClassNotFoundException
异常。
示例流程
假设应用程序类加载器收到了加载 com.example.MyClass
的请求:
-
应用程序类加载器(App ClassLoader)首先会把请求交给它的父类加载器—— 扩展类加载器(Extension ClassLoader)。
-
扩展类加载器(Extension ClassLoader)再将请求交给它的父类加载器—— 启动类加载器(Bootstrap ClassLoader)。
-
启动类加载器(Bootstrap ClassLoader) 查找它负责的
rt.jar
等核心库,如果没有找到com.example.MyClass
,那么它会将控制权返回给 扩展类加载器(Extension ClassLoader)。 -
扩展类加载器(Extension ClassLoader)检查自己负责的扩展目录下的 jar 包,如果没有找到,再将控制权返回给 应用程序类加载器(App ClassLoader)。
-
应用程序类加载器(App ClassLoader)最后检查应用程序的
classpath
目录,如果找到了com.example.MyClass
,则加载该类。
双亲委派模型的优势
-
保证类的唯一性和稳定性:通过双亲委派模型,可以确保一个类在Java虚拟机中只被加载一次,避免了类的重复加载和版本冲突问题。
-
保护核心类库的安全:由于核心Java类库(如java.lang和java.util包中的类)总是由启动类加载器加载,因此可以确保这些类库的安全性和完整性,防止被恶意代码篡改。
-
提高类加载的效率:通过委派机制,类加载器可以复用父类加载器已经加载的类,从而减少重复加载的开销。
打破双亲委派模型
尽管双亲委派模型有许多优点,但它也有一些局限性,特别是在需要动态加载类或者需要实现类的热更新的情况下。例如,在 OSGi
(Open Service Gateway Initiative)框架中,这种模型可能会限制模块之间的隔离性。因此,在某些特定的应用场景中,可能会对双亲委派模型进行一定的调整或使用替代方案。
因此在某些特定场景下,可能需要打破双亲委派模型。
在 Java 入门指南:JVM(Java虚拟机)—— Java 类加载器详解 中,如果我们需要自定义加载器,并想打破双亲委派模型则需要重写 loadClass()
方法。因为类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()
方法来加载类)。
重写 loadClass()
方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。
我们比较熟悉的 Tomcat
服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader
来打破双亲委托机制。
每个 Web 应用都会创建一个单独的 WebAppClassLoader
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader
。各个 WebAppClassLoader
实例之间相互隔离,进而实现 Web 应用之间的类隔。
而 Tomcat 中另一个自定义的类加载器 SharedClassLoader
作为 WebAppClassLoader
的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。
总结
双亲委派模型是 Java 类加载机制中的一个重要概念,它通过委派机制保证了类的唯一性和安全性。然而,在特定场景下,为了满足特定的需求,可能需要打破这一模型。因此,在设计和实现Java应用程序时,需要根据实际情况选择合适的类加载策略。