类加载器是什么?
类加载器是Java虚拟机用来将.class文件(包含Java字节码)从各种来源(如文件系统、网络、压缩包等)加载到内存中,并将其转换成java.lang.Class对象的一个模块。

当Java程序需要使用一个类时,JVM会确保这个类已经被加载、连接(验证、准备、解析)和初始化,这个加载的过程就是由类加载器完成的。
类加载的完整生命周期
一个类的完整生命周期包括七个阶段,但类加载器主要参与前三个阶段:
-
加载:这是类加载器的工作核心,它负责完成三件事:
- 通过一个类的全限定名(如
java.lang.String)来获取定义此类的二进制字节流。 - 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 通过一个类的全限定名(如
-
连接:连接阶段分为三个子步骤。
(图片来源网络,侵删)- 验证:确保加载的
.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,这是非常重要的一步,防止恶意代码的执行。 - 准备:为类的静态变量分配内存,并设置其初始值。注意,这里设置的的是数据类型的零值(如
0,0L,null,false),而不是在代码中显式赋予的值,那个显式的值将在初始化阶段才被赋值。public static int value = 123;,在准备阶段,value的值是0,而不是123。- 但对于常量
public static final int value = 123;,由于final修饰的常量在编译时就已经确定,所以准备阶段它的值就是123。
- 解析:将常量池内的符号引用替换为直接引用,简单说,就是将类、方法、字段等的名称替换为指针或偏移量,这样JVM在执行时就能直接通过内存地址访问它们,而不需要再通过查找符号。
- 验证:确保加载的
-
初始化:这是类加载过程的最后一步,到了这个阶段,JVM才真正开始执行类中定义的Java程序代码,即为静态变量赋予正确的初始值,并执行静态代码块(
static块)。- 只有当主动使用一个类时(如
new一个实例、访问静态变量/方法、反射等),JVM才会确保其被初始化。 - JVM会确保一个类的
<clinit>()方法(由编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并产生)在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。
- 只有当主动使用一个类时(如
双亲委派模型
这是Java类加载器最核心、最重要的工作机制。
类加载器的层次结构
Java中的类加载器有明确的层次关系,形成了一个树状结构,主要有以下几类:
-
启动类加载器
(图片来源网络,侵删)- 也叫引导类加载器,是JVM自身的一部分。
- 它不是
java.lang.ClassLoader的子类,是由C++实现的。 - 负责加载
JAVA_HOME/jre/lib目录下的核心类库,如rt.jar、resources.jar等,或者被-Xbootclasspath参数所指定的路径中的类。 - 它没有父加载器。
-
扩展类加载器
- 是
sun.misc.Launcher$ExtClassLoader的实例。 - 它的父加载器是启动类加载器。
- 负责加载
JAVA_HOME/jre/lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,JDBC的驱动类通常由它加载。
- 是
-
应用程序类加载器
- 也叫系统类加载器,是
sun.misc.Launcher$AppClassLoader的实例。 - 它的父加载器是扩展类加载器。
- 负责加载用户类路径(
Classpath)上所指定的类库,我们自己编写的Java类通常都是由它加载的。 - 程序中可以通过
ClassLoader.getSystemClassLoader()方法获取到它。
- 也叫系统类加载器,是
-
自定义类加载器
- 开发者可以继承
java.lang.ClassLoader类,实现自己的类加载器,用于加载非标准的来源(如网络、加密文件、数据库等)。
- 开发者可以继承
工作流程
双亲委派模型的工作流程如下:
当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。
只有当父加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载。
简单流程图:
+-----------------+
| Custom CL | --(1) 加载请求--> +-----------------+
+-----------------+ | App CL | --(2) 委派--> +-----------------+
+-----------------+ | Ext CL | --(3) 委派--> +-----------------+
+-----------------+ | Bootstrap CL |
+-----------------+
反向流程(查找成功):
+-----------------+
| Custom CL | <--(6) 返回结果-- +-----------------+
+-----------------+ +-----------------+ <--(5) 返回结果-- +-----------------+
| App CL | | Ext CL | <--(4) 返回结果-- +-----------------+
+-----------------+ +-----------------+ | Bootstrap CL |
+-----------------+
工作步骤详解:
- Custom ClassLoader 收到一个加载
com.example.MyClass的请求。 - 它不自己加载,而是将请求委派给它的父加载器 App ClassLoader。
- App ClassLoader 收到请求后,也不自己加载,而是将请求委派给它的父加载器 Ext ClassLoader。
- Ext ClassLoader 收到请求后,同样不自己加载,而是将请求委派给它的父加载器 Bootstrap ClassLoader。
- Bootstrap ClassLoader 开始尝试在
JAVA_HOME/jre/lib下查找com/example/MyClass.class。- 如果找到了:加载该类,然后沿着反向路径(6 -> 5 -> 4 -> 1)将加载好的
Class对象逐层返回,最终完成任务。 - 如果没找到:它会返回加载失败的信息给它的子加载器 Ext ClassLoader。
- 如果找到了:加载该类,然后沿着反向路径(6 -> 5 -> 4 -> 1)将加载好的
- Ext ClassLoader 收到父加载器失败的信息后,开始在自己的搜索范围(
JAVA_HOME/jre/lib/ext)内查找。- 如果找到了:加载并返回。
- 如果没找到:返回失败信息给 App ClassLoader。
- App ClassLoader 收到失败信息后,在自己的搜索范围(用户
Classpath)内查找。- 如果找到了:加载并返回。
- 如果没找到:返回失败信息给 Custom ClassLoader。
- Custom ClassLoader 收到失败信息后,才会尝试调用自己的
findClass()方法去加载,如果自己也无法加载,就会抛出ClassNotFoundException异常。
为什么要使用双亲委派模型?
双亲委派模型主要带来了两大好处:
-
安全性
- 防止核心API被篡改:这个模型确保了Java核心API(位于
rt.jar中)的绝对安全性,试想一下,如果没有这个模型,我可以自己写一个恶意的java.lang.String类,并把它放在Classpath的最前面,当程序中需要使用String时,应用程序类加载器会优先加载我的恶意版本,这会给系统带来巨大的安全风险。 - 双亲委派如何保证安全:由于委派机制,任何加载
java.lang.String的请求最终都会被委派给启动类加载器,而启动类加载器会加载JVM自带的、安全的String类,从而阻止了恶意代码的加载。
- 防止核心API被篡改:这个模型确保了Java核心API(位于
-
避免类的重复加载
- 如果没有委派机制,当两个不同的类加载器都加载同一个类时,内存中就会出现两份
Class对象,这不仅浪费内存,还可能导致类型转换异常(ClassCastException),因为JVM认为它们是两个不同的类。 - 双亲委派如何避免重复:通过委派,同一个类只会被顶层的类加载器加载一次,然后所有子加载器都能共享这个
Class对象,保证了类的唯一性。
- 如果没有委派机制,当两个不同的类加载器都加载同一个类时,内存中就会出现两份
如何打破双亲委派模型?
虽然双亲委派模型是Java推荐的规范,但它并非强制性的,在某些场景下,我们需要打破它。
何时打破?
- 热部署:在Tomcat等Web服务器中,当一个Web应用被更新后,服务器需要能够卸载旧的类,加载新的类,而无需重启整个JVM,Tomcat会为每个Web应用创建一个独立的
WebAppClassLoader,这个加载器会优先加载WEB-INF/classes下的类,而不会委派给父加载器,这样,即使两个Web应用依赖不同版本的同一个库(如commons-logging),它们也能各自加载自己版本的库,而不会互相干扰。 - 代码热更新:在IDE(如IntelliJ IDEA)中,当你修改代码并重新运行时,IDE会利用自定义类加载器加载新的字节码,实现不重启应用就更新代码的功能。
- SPI(Service Provider Interface)机制:例如JDBC、JNDI等,这些接口由Java核心库提供(由Bootstrap ClassLoader加载),但实现类由第三方厂商提供(如MySQL的JDBC驱动),位于应用的
Classpath下,这里就出现了矛盾:接口加载器(Bootstrap)无法委派给实现类加载器(App),为了解决这个问题,Java使用了“线程上下文类加载器”(Thread Context ClassLoader),当核心库需要加载实现类时,它会主动放弃自己的双亲委派,使用线程上下文类加载器(通常是App ClassLoader)去加载。
如何打破?
打破双亲委派模型的核心在于重写 ClassLoader 的 loadClass() 方法,标准的 loadClass() 方法实现了双亲委派的逻辑,如果我们想打破它,可以在自己的类加载器中重写这个方法,在委派给父加载器之前或之后,插入自己的加载逻辑。
Tomcat的 WebAppClassLoader 大致逻辑如下:
// 伪代码
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 检查该类是否已被加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 先尝试加载核心库(委派给父加载器)
try {
return super.loadClass(name, resolve); // 这里面实现了双亲委派
} catch (ClassNotFoundException e) {
// 父加载器找不到,忽略异常,继续下面的步骤
}
// 3. 父加载器找不到,则尝试从自己的路径加载(打破委派)
try {
c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
} catch (ClassNotFoundException e) {
// ...
}
throw new ClassNotFoundException(name);
}
| 特性 | 描述 |
|---|---|
| 核心功能 | 将.class文件加载到内存,生成Class对象。 |
| 生命周期 | 加载 -> 连接(验证、准备、解析) -> 初始化。 |
| 双亲委派模型 | 核心工作机制,先委派给父加载器,父加载器无法完成再由子加载器尝试。 |
| 层次结构 | Bootstrap (启动) -> Ext (扩展) -> App (应用) -> Custom (自定义)。 |
| 主要优点 | 安全性(防止核心API被篡改)、避免类重复加载。 |
| 打破场景 | Web应用热部署、代码热更新、SPI机制(如JDBC)。 |
| 打破方式 | 重写ClassLoader的loadClass()方法。 |
理解Java类加载器的工作原理,特别是双亲委派模型,是成为一名高级Java开发者的必备技能,它不仅能帮助你解决类加载相关的疑难杂症,还能让你在框架开发、应用部署和性能调优中游刃有余。
