类加载器
首先回顾一下类加载过程。加载 -> 验证 -> 准备 -> 解析 -> 初始化
加载是类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class对象,作为方法区这些数据的访问入口
类加载器介绍
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰,反正是一种嵌入在网页中的特殊程序,用于生成动态内容,跟 JSP 差不多) 的需要。
后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
类加载器是一个负责加载类的对象。
ClassLoader是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。每个 Java 类都有一个引用指向加载它的
ClassLoader。不过,数组类不是通过ClassLoader创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取ClassLoader的时候和该数组的元素类型的ClassLoader是一致的。来自官方文档
这部分是什么意思呢?
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个 Java 类都有一个引用指向加载它的
ClassLoader。 - 数组类不是通过
ClassLoader创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
一句话:类加载器是 JVM 的一部分,负责在运行时动态地将 Java
类的字节码(.class 文件)加载到内存中,并生成对应的
java.lang.Class 对象。
这个过程发生在 类加载阶段的“加载”(Loading)步骤。
其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。
类加载器加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。
用到什么,加载什么
对于已经加载的类会被放在 ClassLoader
中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
这个检查是否被加载过,对于一个类加载器来说,是判断两个类是否有相同二进制名称
对于一个类加载器来说,相同二进制名称的类只会被加载一次。
或者这么说,只有在两个类是由同一个类加载器加载的前提下,比较两个类相等才有意义,因为即使这两个类来源于同一个 Class 文件,在同一个 JVM 上被加载,但是它们的类加载器不同,它们也是不同的类(这里的相等包括 类的 Class 对象的 equals() 方法,newInstance()方法等返回的结果是不是一样的)
JVM 内置的三大类加载器
Bootstrap ClassLoader
顾名思义,启动类加载器。是使用 C++ 实现的,属于 JVM 运行时的一部分
加载路径
<JAVA_HOME>/lib下的核心 JAR(如rt.jar,resources.jar,charsets.jar等)- 通过
-Xbootclasspath指定的额外路径
加载内容:Java 最核心的类,比如:
java.lang.Objectjava.lang.Stringjava.util.ArrayListjava.io.File- 所有
java.*包下的基础类(除部分模块化后的例外)
Extension ClassLoader
Extension ClassLoader,扩展类加载器,Java 9+ 改名为 Platform ClassLoader
主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar
包和类以及被 java.ext.dirs
系统变量所指定的路径下的所有类。
父加载器虽然是Bootstrap ClassLoader,但 Java 中无法直接引用,所以其
parent 字段为 null
注意,JDK 9+ 移除了 ext/ 目录机制,引入
模块系统(JPMS),它现在负责加载 Java 平台模块,所以被改名成了 Platform
ClassLoader,只有 java.base 模块仍由 Bootstrap ClassLoader
加载,因为它是所有模块的基础
AppClassLoader
应用程序类加载器 ,或者也叫 System ClassLoader
它的父加载器是 ExtensionClassLoader,它加载 由
-classpath 或 -cp 指定的路径,即我们日常开发的
.class 文件和第三方
JAR,是我们写的绝大多数类的默认加载器。
自定义类加载器
除了 BootstrapClassLoader 是 JVM
自身的一部分之外,其他所有的类加载器都是在 JVM
外部实现的,并且全都继承自
ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader
可以通过getParent()获取其父
ClassLoader,如果获取到 ClassLoader
为null的话,那么该类加载器的父类加载器是
BootstrapClassLoader 。
为什么 获取到
ClassLoader为null就是BootstrapClassLoader加载的呢?这是因为
BootstrapClassLoader由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,Bootstrap ClassLoader 不是 Java 对象,在 Java 中没有对应的实例,所以拿到的结果肯定是 null。
1 | public class PrintClassLoaderTree { |
- 从输出结果可以看出:
- 我们编写的 Java 类
PrintClassLoaderTree的ClassLoader是AppClassLoader; AppClassLoader的父ClassLoader是ExtClassLoader;ExtClassLoader的父ClassLoader是Bootstrap ClassLoader,因此输出结果为 null
- 我们编写的 Java 类
可以用以下代码验证哪些类由 Bootstrap 加载
1 | public class CheckBootstrapClasses { |
嗯嗯回到正题,前面提到,如果我们要自定义自己的类加载器,很明显需要继承
ClassLoader抽象类。
ClassLoader 类有两个关键的方法:
protected Class<?> loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name为类的二进制名称,resolve如果为 true,在加载时调用resolveClass(Class<?> c)方法解析该类。
protected Class<?> findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的
findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写
loadClass() 方法。
那么问题来了,什么是双亲委派模型
双亲委派模型
双亲委派模型介绍
类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
ClassLoader类使用委托模型来搜索类和资源。每个ClassLoader实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 虚拟机中被称为 “bootstrap class loader”的内置类加载器本身没有父类加载器,但是可以作为ClassLoader实例的父类加载器。
从上面官方文档的介绍可以看出:
ClassLoader类使用委托模型来搜索类和资源。- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
也就是说,不是谁“想”加载就加载,而是遵循一套严格的“先问上级行不行,上级不行自己上”的规则。这就是双亲委派模型。
每个类加载器在“自己动手”之前,必须先让父加载器尝试。只有父加载器明确“找不到”,子加载器才自己加载。
我一直感觉中文翻译有点误导。“双亲”并不是指两个父级,而是指 “向上委托给父级” 的层级结构。
下图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
为什么要这样?
- 安全性,防止用户自定义各种来替换 JDK 核心类(因为
String加载请求会先到 Bootstrap,它已经在 rt.jar 中找到了,直接返回,根本不会让你的 AppClassLoader 有机会加载同名类) - 避免重复加载,同一个类只会被一个 ClassLoader
加载一次。如果没有委派,AppClassLoader 和 ExtClassLoader
可能各自加载同一个类,导致
instanceof判断失败等严重问题 - 层次清晰,职责分明
双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型,也是可以的,但是通常情况下,我们不打破
另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
因为在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承
双亲委派模型的执行流程
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在
java.lang.ClassLoader 的 loadClass()
中,相关代码如下所示。
1 | protected Class<?> loadClass(String name, boolean resolve) |
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader中。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()方法来加载类)。如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException异常。
双亲委派模型的好处
上面说了,双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了安全目标,避免类的重复加载和防止核心 API 被篡改。
别忘了,JVM 区分不同类的依据是类名加上加载该类的类加载器,即使类名相同,如果由不同的类加载器加载,也会被视为不同的类。也就是说,你一个类被两个不同的类加载器加载了,会判定为不一样,这会出大问题。
双亲委派模型确保核心类总是由 BootstrapClassLoader
加载,保证了核心类的唯一性。
然而,即使攻击者绕过了双亲委派模型,Java
仍然具备更底层的安全机制来保护核心类库。ClassLoader 的
preDefineClass 方法会在定义类之前进行类名校验。任何以
"java." 开头的类名都会触发
SecurityException,阻止恶意代码定义或加载伪造的核心类。
打破双亲委派模型
自定义加载器的话,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的
findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写
loadClass() 方法
因为,上面源码提到了,把这个请求委派给父类加载器去完成的本质上是调用父加载器
loadClass()方法来加载类
重写
loadClass()方法之后,我们就可以改变传统双亲委派模型的执行流程。
例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。
我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web
应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器
WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web
应用之间的类实现隔离的具体原理。
从图中的委派关系中可以看出:
CommonClassLoader作为CatalinaClassLoader和SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用。因此,CommonClassLoader是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。CatalinaClassLoader用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。SharedClassLoader作为WebAppClassLoader的父加载器,专门来加载 Web 应用之间共享的类,但是在Tomcat的默认配置下catalina.properties配置文件的shared.loader=值为空,所以SharedClassLoader并不生效,SharedClassLoader实际上会退化为CommonClassLoader,SharedClassLoader比较合适用来加载多个web应用间共享的类库,比如整个公司级别的监控、日志等。- 每个 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加载。再比如,Tomcat 启动 其类启动器
SharedClassLoader加载$ CATALINA_HOME/lib下的共享 JAR(如spring-core.jar),每个 Web 应用(如myapp.war)有自己的WebAppClassLoader,加载WEB-INF/classes和WEB-INF/lib下的业务类,也就是一般情况下我们编写的业务代码,但是,我们通常会在我们的业务代码中使用 Spring 的注解1
2
3
4
public class UserService {
// 使用了 Spring 的 @Service 注解
}@Service是由SharedClassLoader加载的(来自 spring-core.jar)UserService是由WebAppClassLoader加载的(在你的 WAR 包里)
矛盾出现了:Spring 框架(由 SharedClassLoader 加载)在运行时需要动态加载你的
UserService类,但SharedClassLoader的加载路径 不包含WEB-INF/classes!根据双亲委派,它也无法“向下”委托给WebAppClassLoader(委派只能向上!)
针对第二种情况,我们需要让高层框架(Spring)放弃用自己的类加载器,转而使用当前线程绑定的“应用专属”类加载器,这也就是 线程上下文类加载器 的核心思想
1 | ClassLoader cl = Thread.currentThread().getContextClassLoader(); |
每个 Thread 对象内部有一个私有字段:
默认值是主线程的上下文加载器 AppClassLoader,新创建的线程
继承父线程的上下文类加载器,Web 容器会在处理每个 Web
应用请求前,显式设置该线程的上下文类加载器为WebAppClassLoader
那么 Spring 的典型做法就是这样
1 | // Spring 源码中的典型做法 |
这是一种 “逆向委派”,本质上打破了严格的父子层级限制,实现了 跨类加载器的协作,允许跨层级加载。
那么,讲解是以第二种情况讲解的,但是第一种情况的 JDBC 也可以这样被解决,只不过 JDK 的更新模块化顺手把这个事情鼓捣好了,以前 JDBC 也是这么干的







