类加载器详解(重点)
回顾一下类加载过程
开始介绍类加载器和双亲委派模型之前,简单回顾一下类加载过程。
- 类加载过程:加载->连接->初始化。
- 连接过程又可分为三步:验证->准备->解析。

加载是类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class对象,作为方法区这些数据的访问入口
类加载器
类加载器介绍
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
根据官方 API 文档的介绍:
A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.
Every Class object contains a reference to the ClassLoader that defined it.
Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.
翻译过来大概的意思是:
类加载器是一个负责加载类的对象。
ClassLoader是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。每个非数组类或接口都有一个引用指向定义它的
ClassLoader。数组类不是通过ClassLoader创建的,而是 JVM 在需要时自动创建;引用类型数组的类加载器与其组件类型一致,基本类型数组则没有类加载器,getClassLoader()返回null。
从上面的介绍可以看出:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个非数组类或接口都记录定义它的
ClassLoader。 - 数组类不是通过
ClassLoader创建的(数组类没有对应的二进制字节流),而是由 JVM 直接生成;引用类型数组沿用组件类型的定义类加载器,基本类型数组没有类加载器。
class Class<T> {
...
private final ClassLoader classLoader;
@CallerSensitive
public ClassLoader getClassLoader() {
//...
}
...
}简单来说,类加载器的主要作用就是动态加载 Java 类的字节码(.class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。 字节码可以是 Java 源程序(.java 文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。
其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。
类加载器加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
类加载时,ClassLoader#loadClass 会先通过 findLoadedClass 判断 JVM 是否已将当前类加载器记录为该二进制名称对应类的发起加载器,命中后直接返回,否则才继续委派或查找。一个类加载器不能重复定义同一二进制名称的类。
// 以下字段和方法来自 JDK 8 的 ClassLoader 实现,仅用于说明该版本的记录方式
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。
private final Vector<Class<?>> classes = new Vector<>();
// 由VM调用,用此类加载器记录每个已加载类。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}类加载器总结
JDK 8 中常见的三个重要类加载器如下:
BootstrapClassLoader(启动类加载器):顶层的虚拟机内置加载器,在 Java API 中通常表示为null,并且没有父加载器。在 JDK 8 HotSpot 中,它主要加载运行时核心类库(如rt.jar)以及-Xbootclasspath指定路径中的类。ExtensionClassLoader(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext目录下的 jar 包和类以及被java.ext.dirs系统变量所指定的路径下的所有类。AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
🌈 拓展一下:
rt.jar:rt 代表“RunTime”,rt.jar是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。- Java 9 引入模块系统后不再使用
rt.jar和扩展目录机制,扩展类加载器由平台类加载器(Platform Class Loader)取代。启动、平台和应用类加载器分别定义不同的运行时模块;不能简单概括为除java.base外的所有模块都由平台类加载器加载。
除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码(.class 文件)进行加密,加载时再利用自定义的类加载器对其解密。

启动类加载器是虚拟机内置的,通常在 Java API 中表示为 null。平台类加载器、应用类加载器以及通常的自定义类加载器都是 ClassLoader 的实例。这样用户可以自定义类加载器,以便让应用程序决定如何获取所需的类。
每个 ClassLoader 可以通过 getParent() 获取其父 ClassLoader,如果获取到的 ClassLoader 为 null 的话,那么该类加载器的父类加载器是 BootstrapClassLoader。
public abstract class ClassLoader {
...
// 父加载器
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
//...
}
...
}为什么由启动类加载器定义的类调用 getClassLoader() 会得到 null 呢? 因为启动类加载器是 JVM 内置类加载器,Java API 约定通常用 null 表示它;具体实现语言并不是 Java 规范的一部分。
下面我们来看一个获取 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的父ClassLoader是ExtClassLoader;ExtClassLoader的父ClassLoader是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):根据类的二进制名称查找类,默认实现直接抛出ClassNotFoundException。
官方 API 文档中写到:
Subclasses of
ClassLoaderare encouraged to overridefindClass(String name), rather than this method.建议
ClassLoader的子类重写findClass(String name)方法而不是loadClass(String name, boolean resolve)方法。
如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
双亲委派模型
双亲委派模型介绍
类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
根据官网介绍:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
翻译过来大概的意思是:
ClassLoader类使用委托模型来搜索类和资源。每个ClassLoader实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为ClassLoader实例的父类加载器。
从上面的介绍可以看出:
ClassLoader类使用委托模型来搜索类和资源。- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
下图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。

注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型,也是可以的,后文会介绍具体的方法。
其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 MotherClassLoader 和一个 FatherClassLoader。个人觉得翻译成单亲委派模型更好一些,不过,国内既然翻译成了双亲委派模型并流传了,按照这个来也没问题,不要被误解了就好。
另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
public abstract class ClassLoader {
...
// 组合
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
...
}在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。
双亲委派模型的执行流程
双亲委派模型的主要逻辑集中在 java.lang.ClassLoader 的 loadClass() 中。下面展示的是 JDK 8 的相关实现片段:
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()方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException异常。
🌈 拓展一下:
JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
双亲委派模型的好处
双亲委派模型是 Java 类加载机制的重要组成部分。父加载器优先有助于让同一委派链中的代码复用父加载器已经定义的类型,并降低应用代码冒充平台 API 的风险;它并不能阻止相互独立的类加载器分别定义同名类。
JVM 区分运行时类型的依据包括类或接口的二进制名称以及定义它的类加载器。双亲委派会让平台类优先由相应的内置类加载器定义,但并非所有核心或平台 API 都由启动类加载器加载:JDK 9 以后还有平台类加载器负责定义平台类。
例如,JVM 会优先将 java.lang.Object 这类核心类的加载请求交给 BootstrapClassLoader 处理;但实际上,ClassLoader#preDefineClass 还会在定义阶段校验类名,任何以 java. 开头的类名都会被拒绝,因此不能通过自定义加载器去伪造核心类。
有很多小伙伴就要说了:“那我绕过双亲委派模型不就可以了么?”。
然而,即使攻击者绕过了双亲委派模型,Java 仍然具备更底层的安全机制来保护核心类库。ClassLoader 的 preDefineClass 方法会在定义类之前进行类名校验。任何以 "java." 开头的类名都会触发 SecurityException,阻止恶意代码定义或加载伪造的核心类。
JDK 8 中 ClassLoader#preDefineClass 方法源码如下:
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
// 检查类名是否合法
if (!checkName(name)) {
throw new NoClassDefFoundError("IllegalName: " + name);
}
// 防止在 "java.*" 包中定义类。
// 此检查对于安全性至关重要,因为它可以防止恶意代码替换核心 Java 类。
// JDK 9 利用平台类加载器增强了 preDefineClass 方法的安全性
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("禁止的包名: " +
name.substring(0, name.lastIndexOf('.')));
}
// 如果未指定 ProtectionDomain,则使用默认域(defaultDomain)。
if (pd == null) {
pd = defaultDomain;
}
if (name != null) {
checkCerts(name, pd.getCodeSource());
}
return pd;
}JDK 9 引入了平台类加载器,可以通过 ClassLoader.getPlatformClassLoader() 获取。defineClass 对 java.* 包名的限制仍然存在,但具体实现代码与 JDK 8 不同。
打破双亲委派模型方法
为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 loadClass() 即可。
🐛 修正(参见:issue871):自定义加载器的话,需要继承 ClassLoader。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
为什么是重写 loadClass() 方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了:
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()方法来加载类)。
重写 loadClass() 方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。
我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
Tomcat 的类加载器的层次结构如下:

现代 Tomcat 默认使用 Bootstrap -> System -> Common -> WebappX 层次。各加载器的搜索位置如下:
Common的搜索位置由$CATALINA_BASE/conf/catalina.properties中的common.loader配置,默认主要包括$CATALINA_BASE/lib和$CATALINA_HOME/lib。- 每个
WebappX加载器负责相应 Web 应用的/WEB-INF/classes和/WEB-INF/lib/*.jar。 Server和Shared加载器默认并未定义;只有配置server.loader或shared.loader后才会出现在更复杂的层次中。
从图中的委派关系中可以看出:
Common加载器可见的类可以由 Tomcat 内部组件和所有 Web 应用共享。- 如果显式配置,
Server只对 Tomcat 内部可见,Shared则对所有 Web 应用可见。默认配置没有这两个加载器,Web 应用加载器直接以Common为父加载器。 - 每个 Web 应用都会创建一个单独的
WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为WebAppClassLoader。各个WebAppClassLoader实例之间相互隔离,进而实现 Web 应用之间的类隔。
单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
比如,SPI 中,SPI 的接口(如 java.sql.Driver)是由 Java 核心库提供的,由 BootstrapClassLoader 加载。而 SPI 的实现(如 com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(BootstrapClassLoader)也会用来加载 SPI 的实现。按照双亲委派模型,BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。
这里需要注意:JDK 9+ 之后引入模块化,JDBC API 被拆分到 java.sql 模块中,不再是 BootstrapClassLoader 直接加载,而是由 PlatformClassLoader 加载。
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("java.sql.Driver");
ClassLoader loader = clazz.getClassLoader();
System.out.println("Loader for java.sql.Driver: " + loader);
// .jdks/corretto-1.8.0_442/bin/java 环境下为 Loader for java.sql.Driver: null
// .jdks/jbr-17.0.12/bin/java 环境下为 Loader for java.sql.Driver: jdk.internal.loader.ClassLoaders$PlatformClassLoader@30f39991
}
}再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,所以 SharedClassLoader 无法找到业务类,也就无法加载它们。
如何解决这个问题呢? 这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader) 了。
拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader。这样就可以让高层的类加载器(SharedClassLoader)借助子类加载器(WebAppClassLoader)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。
线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。
Java.lang.Thread 中的 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 分别用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl) 进行设置的话,线程将继承其父线程的上下文类加载器。
Spring 获取线程线程上下文类加载器的代码如下:
cl = Thread.currentThread().getContextClassLoader();感兴趣的小伙伴可以自行深入研究一下 Tomcat 打破双亲委派模型的原理,推荐资料:《深入拆解 Tomcat & Jetty》。
推荐阅读
- 《深入拆解 Java 虚拟机》
- 深入分析 Java ClassLoader 原理:https://blog.csdn.net/xyang81/article/details/7292380
- Java 类加载器(ClassLoader):http://gityuan.com/2016/01/24/java-classloader/
- Class Loaders in 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
写在最后
如果内容对你有帮助的话,欢迎顺手给 JavaGuide 点一个免费的 Star 支持一下:GitHub | Gitee。
JavaGuide 已持续维护近七年,累计 6100+ 次提交,来自 620+ 位贡献者共同完善。你的 Star、反馈和 PR,都是这个项目继续更新的动力。
如果你正在准备后端/AI 应用开发面试,也可以了解一下我的知识星球,里面包括后端和 AI 实战项目、简历优化、一对一提问和高频考点资料,已经持续维护六年。
