这是携程一面的一个关于Java虚拟机(JVM)的面试真题。对于参加过校园招聘面试的同学来说,这个问题应该并不陌生。当询问关于JVM的知识点时,常常会涉及到双亲委派模型(Parent Delegation Model),尽管这个翻译略显别扭。

学习双亲委派模型不仅对面试准备有帮助,也对我们理解Java的类加载机制至关重要。以Tomcat服务器为例,它为了实现Web应用的隔离,自定义了类加载器并打破了双亲委派模型。
在本文中,我将首先介绍类加载器的概念,然后深入探讨双亲委派模型,以便更好地理解这一机制。
目录概览:

类加载过程回顾
在详细讲解类加载器和双亲委派模型之前,我们先简单回顾类加载的过程:
- 类加载过程:加载 -> 连接 -> 初始化
- 连接过程进一步分为三步:验证 -> 准备 -> 解析
在类加载的第一步中,我们主要完成以下三项工作:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class对象,作为对这些数据的访问入口。
类加载器的概述
类加载器介绍
类加载器自JDK 1.0就已存在,最初是为了满足Java Applet(已淘汰)的需求,后来逐渐发展成Java程序的重要组成部分,使Java类能够动态加载并执行。
根据官方API文档的定义:
类加载器是负责加载类的对象。
ClassLoader是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。一个典型的策略是将名称转换为文件名,然后从文件系统中读取相应的“类文件”。每个Class对象都包含一个指向定义它的ClassLoader的引用。
数组类的Class对象不是由类加载器创建,而是由Java运行时在需要时自动生成的。如果元素类型是基本类型,则数组类没有类加载器。
总结以上内容:
- 类加载器负责加载类,是类加载过程中的关键环节。
- 每个Java类都关联有其加载器。
- 数组类由JVM直接生成,而非由类加载器创建。
简单来说,类加载器的主要功能是将Java类的字节码(.class 文件)加载到JVM中,生成内存中的 Class 对象。字节码来源于Java源程序(.java 文件)经过 javac 编译,或通过其他工具动态生成或网络下载。
除了加载类,类加载器还负责加载Java应用所需的资源,如文本、图像、配置文件等。本文将主要讨论加载类的核心功能。
类加载器加载规则
JVM启动时不会一次性加载所有类,而是根据需求动态加载。大部分类在实际使用时才会被加载,这在内存管理上更为高效。
对于已加载的类,会存放在 ClassLoader 中。在加载类时,系统会先检查该类是否已被加载,如果已加载则直接返回,否则再尝试加载。每个类加载器只会加载同一二进制名称的类一次。
类加载器总结
JVM内置了以下三种重要的 ClassLoader:
BootstrapClassLoader(启动类加载器):最顶层的加载器,由C++实现,通常表示为null,主要用于加载JDK内部核心类库(如%JAVA_HOME%/lib目录下的rt.jar、resources.jar、charsets.jar等)及由-Xbootclasspath参数指定的路径下的类。ExtensionClassLoader(扩展类加载器):负责加载%JRE_HOME%/lib/ext目录下的jar包和类,以及由java.ext.dirs系统变量指定路径的所有类。AppClassLoader(应用程序类加载器):面向用户的加载器,负责加载当前应用classpath下的所有jar包和类。
🌈 拓展:
rt.jar:Java的基础类库,包含我们在Java文档中看到的所有类文件,常用的内置库java.xxx.*都在其中,如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。- Java 9引入了模块系统,对类加载器的架构稍作调整,扩展类加载器被称为平台类加载器(platform class loader),大多数模块除
java.base由启动类加载器加载外,其他模块均由平台类加载器加载。
用户可自定义类加载器以满足特定需求,如加密类字节码并在加载时解密。
除了 BootstrapClassLoader 是JVM的一部分外,其他类加载器均在JVM外部实现,并继承自 ClassLoader 抽象类。这种设计允许用户自定义类加载器,让应用程序决定如何获取所需的类。
每个 ClassLoader 可以通过 getParent() 方法获取其父类加载器,若返回null,则说明该类是通过 BootstrapClassLoader 加载的。
为何获取到的 ClassLoader 为null则是 BootstrapClassLoader 加载的? 因为 BootstrapClassLoader 是用C++实现的,因此在Java中没有对应的类表示它,返回结果为null。
接下来,我们来看一个获取 ClassLoader 的简单示例:
public class PrintClassLoaderTree {
public static void main(String[] args) {
ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue) {
System.out.println(split.toString() + classLoader);
if (classLoader == null) {
needContinue = false;
} else {
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}
输出结果(JDK 8):
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
从输出结果中我们可以得知:
- 我们编写的Java类
PrintClassLoaderTree的ClassLoader是AppClassLoader; AppClassLoader的父加载器是ExtClassLoader;ExtClassLoader的父加载器是Bootstrap ClassLoader,因此输出结果为null。
自定义类加载器
如前所述,除了 BootstrapClassLoader,其他类加载器均由Java实现,且全部继承自 java.lang.ClassLoader。若需自定义类加载器,需继承 ClassLoader 抽象类。
ClassLoader 类包含两个关键方法:
protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,遵循双亲委派机制。name为类的二进制名称,resolve为true时,会在加载时调用resolveClass(Class<?> c)方法解析该类。protected Class findClass(String name):根据类的二进制名称查找类,默认实现为空。
官方API文档建议:
ClassLoader的子类应重写findClass(String name)方法而非loadClass(String name, boolean resolve)方法。
如果希望保持双亲委派模型,只需重写 findClass() 方法。相反,若需打破双亲委派模型,则需重写 loadClass() 方法。
双亲委派模型介绍
类加载器有多种,当我们想加载一个类时,具体由哪个类加载器负责加载,就需要涉及到双亲委派模型。
根据官网说明:
ClassLoader类使用委托模型来搜索类和资源。每个ClassLoader实例都有一个相关的父类加载器。当请求查找类或资源时,ClassLoader实例会在亲自查找类或资源之前,将搜索任务委托给其父类加载器。虚拟机的内置类加载器,称为“启动类加载器”,没有自己的父类,但可以作为ClassLoader实例的父类加载器。
翻译成中文的意思是:
ClassLoader类使用一种委托模型来查找类和资源。每个ClassLoader实例都与一个父类加载器相关联。当请求查找类或资源时,ClassLoader实例会在尝试自己查找之前,先将查找任务委托给其父类加载器。虚拟机中的内置类加载器称为“启动类加载器”,它本身没有父类,但可以作为其他ClassLoader实例的父类加载器。
从上述解释中可以看出:
ClassLoader类使用委托模型来搜索类和资源。- 双亲委派模型要求除了顶层的启动类加载器外,其余类加载器均应有父类加载器。
ClassLoader实例在亲自查找类或资源之前,会先将任务委托给父类加载器。
以下图展示的各类加载器之间的层次关系,被称为类加载器的“双亲委派模型(Parent Delegation Model)”。
需要注意的是⚠️:双亲委派模型并不是强制性的约束,而是JDK官方推荐的方式。如果由于特殊需求需要打破双亲委派模型,也是可以的。对于翻译上,双亲的理解很容易让人误解。这里的双亲更多指的是“父辈”的概念,而不是真正的“父母”。我个人认为将其理解为单亲委派模型更加恰当,但既然国内普遍称为双亲委派模型,那么继续使用这一称谓即可,要避免误解即可。
此外,类加载器之间的父子关系并不是通过继承实现的,而是通常通过组合关系来复用父加载器的代码。
public abstract class ClassLoader {
...
// 组合
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
...
}
在面向对象编程中,有一条经典设计原则:组合优于继承,多用组合少用继承。
双亲委派模型的执行流程
双亲委派模型的实现逻辑简单明了,集中在 java.lang.ClassLoader 的 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;
}
}
每当一个类加载器接收到加载请求时,它会首先将请求转发给其父类加载器。只有在父类加载器未能找到请求的类时,该加载器才会尝试自己加载。
根据上述源码,我们可以简单总结双亲委派模型的执行流程如下:
- 在类加载时,系统首先判断当前类是否已经被加载,若已加载则直接返回,否则尝试加载。每个父类加载器都会经历这个流程。
- 类加载器在加载类时,通常不会直接尝试加载,而是将该请求委托给父类加载器(调用父加载器的
loadClass()方法)。这样,所有请求最终都会传递到顶层的启动类加载器BootstrapClassLoader。 - 仅当父加载器反馈无法完成加载请求(在其搜索范围内没有找到所需类)时,子加载器才会尝试调用自己的
findClass()方法进行加载。
🌈 拓展一下:
JVM 判定两个Java类是否相同的规则:JVM不仅会检查类的全名,还会检查加载此类的类加载器是否相同。只有在两者都相同的情况下,才认为两个类是相同的。即使两个类来源于同一个 .class 文件,且被同一虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不同。
双亲委派模型的优点
双亲委派模型保证了Java程序的稳定运行,避免了类的重复加载(JVM区分不同类的不仅依据类名,相同的类文件被不同的类加载器加载会产生两个不同的类),同时确保了Java的核心API不被篡改。
如果不使用双亲委派模型,而是让每个类加载器自行加载,那么可能会出现多个不同的 java.lang.Object 类。双亲委派模型确保加载的是JRE中的 Object 类,而不是用户自定义的 Object 类。这是因为 AppClassLoader 在加载用户的 Object 类时,会委托给 ExtClassLoader,而 ExtClassLoader 会继续委托给 BootstrapClassLoader。后者发现自己已加载过 Object 类,因此直接返回,不会加载用户定义的版本。
打破双亲委派模型的方法
若要自定义加载器,需继承 ClassLoader。如果希望保持双亲委派模型,只需重写 ClassLoader 类中的 findClass() 方法。无法由父类加载器加载的类最终将通过此方法被加载。若想打破双亲委派模型,则需重写 loadClass() 方法。
为何重写 loadClass() 方法会打破双亲委派模型呢?如前所述,双亲委派模型的执行流程表明:
类加载器在进行类加载时,首先不会尝试自己加载,而是把请求委派给父类加载器。
例如,Tomcat服务器自定义了 WebAppClassLoader,以便优先加载Web应用目录下的类,从而打破双亲委派机制。这也是Tomcat实现Web应用之间类隔离的具体原理。
对Tomcat的类加载器层次结构感兴趣的朋友可以自行研究,这将有助于更好地理解Tomcat中Web应用的隔离原理。推荐阅读《深入拆解Tomcat & Jetty》。
推荐阅读
- 深入分析Java ClassLoader原理:https://blog.csdn.net/xyang81/article/details/7292380
- Java类加载器(ClassLoader):http://gityuan.com/2016/01/24/java-classloader/
- Java中的类加载器:https://www.baeldung.com/java-classloaders
- Class ClassLoader - Oracle 官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html
- 老大难的Java ClassLoader再不理解就老了:https://zhuanlan.zhihu.com/p/51374915