作为 Java 开发者,我们每天都在写 class,每天都在 new 对象。但你是否想过,硬盘上那个静静躺着的 .class 字节码文件,究竟是如何变成内存中活生生的对象的?
这一切的幕后推手,就是 类加载器 (ClassLoader) 。而面试中最高频的考点------双亲委派机制,则是它行事的最高准则。
今天我们就来拆解一下这个机制,看看它是如何工作的,为什么要这么设计,以及 Tomcat 是如何"叛逆"地打破它的。
一、 什么是类加载器?
简单来说,类加载器的作用就是将字节码文件加载到 JVM 内存中。
在 JVM 的世界里,类加载器有着森严的等级制度。主要分为以下几类(按优先级从高到低):
-
启动类加载器 (Bootstrap ClassLoader)
-
身份:幕后的大 Boss。
-
来源:它不是 Java 类,而是由 C/C++ 实现的,嵌在 JVM 内核中。
-
职责 :负责加载 Java 的核心类库,即
JAVA_HOME/jre/lib目录下的类(比如rt.jar中的String,Object等)。 -
特点:Java 代码无法直接访问它。
-
-
扩展类加载器 (Extension ClassLoader)
-
身份:核心部门经理。
-
来源:Java 实现。上级是 Bootstrap。
-
职责 :负责加载
JAVA_HOME/jre/lib/ext目录下的扩展类库。
-
-
应用程序类加载器 (Application ClassLoader)
-
身份:干活的普通员工。
-
来源:Java 实现。上级是 Extension。
-
职责 :负责加载
classpath目录下(即我们自己写的代码和引入的第三方 jar 包)的类。
-
-
自定义类加载器 (Custom ClassLoader)
-
身份:特种部队/外包团队。
-
职责:根据特殊需求(如类隔离、热部署、加载加密 class)自定义加载规则。上级通常是 Application。
-

二、 什么是双亲委派机制?
了解了等级制度,我们再来看看它们是如何协同工作的。
双亲委派机制 指明了类加载器在加载类时的行为准则。
1. 工作过程(推卸责任流)
当一个类加载器收到了类加载的请求时,它的动作如下:
-
不自己干:它首先不会自己去尝试加载这个类。
-
找上级 :它会把这个请求委派给父类加载器去完成。
-
层层传递 :每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的 Bootstrap ClassLoader 中。
-
兜底处理:只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
注意:这里的"父子关系"通常不是通过 Java 的类继承(Inheritance)实现的,而是通过组合(Composition)复用父加载器的代码。
2. 为什么要这么设计?(核心好处)
这种"层层上报"的机制看似麻烦,实则为了解决两个核心问题:
-
保证类的安全性(沙箱安全机制)
-
防止核心类被篡改。假设黑客自定义了一个
java.lang.String类并试图加载。如果没有双亲委派,这个类可能被 AppClassLoader 加载,从而替换掉 JDK 原生的 String 类,导致系统崩溃或泄露信息。 -
有了双亲委派,请求最终会传给 Bootstrap,Bootstrap 发现它已经加载过核心的 String 类了,就会直接返回,黑客的伪造类根本没有机会被加载。
-
-
保证类的唯一性
- 避免重复加载。当父加载器已经加载了该类时,子加载器就没必要再加载一次。保证内存中只有一个
java.lang.Object对象。
- 避免重复加载。当父加载器已经加载了该类时,子加载器就没必要再加载一次。保证内存中只有一个
三、 类加载器体系:等级森严的"职场架构"(非重点)
在 JVM 中,类加载器的作用是将硬盘上的 .class 字节码文件加载到内存中。JVM 设计了一套严格的层级架构,主要包含三个核心的类加载器。
要理解它们,我们必须先理清一个核心概念:它们在"血缘"上(代码继承)和"职位"上(双亲委派)是两套完全不同的关系。
1. 三大核心类加载器
(1) 启动类加载器 (Bootstrap ClassLoader) ------ "幕后的大 Boss"
-
职责:负责加载 Java 最核心的类库。
- 加载路径:
JAVA_HOME/jre/lib(例如rt.jar,包含java.lang.String、java.util.List等)。
- 加载路径:
-
实现语言 :C/C++。
- 它是 JVM 内核的一部分,不是 Java 类。
-
特点 :因为它是 C++ 写的,Java 代码无法直接获取到它。如果你尝试打印
String.class.getClassLoader(),结果是null。
(2) 扩展类加载器 (Extension ClassLoader) ------ "部门经理"
-
职责:负责加载扩展功能的类库。
- 加载路径:
JAVA_HOME/jre/lib/ext。
- 加载路径:
-
实现语言 :Java。
- 它是
sun.misc.Launcher$ExtClassLoader类的实例。
- 它是
-
上级:Bootstrap ClassLoader。
(3) 应用程序类加载器 (Application ClassLoader) ------ "普通员工"
-
职责:负责加载用户代码和第三方库。
- 加载路径:
classpath(即我们项目中的 class 文件和 Maven 引入的 jar 包)。
- 加载路径:
-
实现语言 :Java。
- 它是
sun.misc.Launcher$AppClassLoader类的实例。
- 它是
-
上级:Extension ClassLoader。
2. 核心难点:继承关系 vs 委派关系
这是最容易混淆的地方。请看下图,左边是代码层面的"继承",右边是运行时的"委派"。
区别一:谁是 Java 类?谁不是?
-
AppClassLoader 和 ExtClassLoader 都是 Java 类 。它们在代码中都继承 自
java.lang.ClassLoader(这个抽象类定义了loadClass方法)。 -
Bootstrap ClassLoader 是 C++ 程序 。它没有继承
java.lang.ClassLoader(因为 C++ 无法继承 Java 类)。它是 JVM 自带的底层加载机制。
区别二:父子关系的本质
我们在双亲委派中说的"父加载器(Parent)",指的不是 类继承(Extends)关系,而是组合(Composition)关系。
-
代码层面(继承) : AppClassLoader 和 ExtClassLoader 是兄弟关系,它们都继承自
URLClassLoader,最终继承自java.lang.ClassLoader。 -
逻辑层面(委派): JVM 在启动时,通过代码将它们组装成上下级:
-
AppClassLoader 的
parent属性被设置为 ExtClassLoader。 -
ExtClassLoader 的
parent属性被设置为null(因为 Bootstrap 是 C++ 写的,Java 引用不到,所以用 null 代表它)。
-
3. 自定义类加载器 (Custom ClassLoader)
除了 JVM 自带的三个,我们还可以根据需求实现自定义类加载器。
-
实现方式 :编写一个 Java 类,继承
java.lang.ClassLoader。 -
应用场景:
-
类隔离:如 Tomcat,为了隔离不同 Web 应用的同名类。
-
加密保护:加载加密过的 class 文件,在内存中解密。
-
热部署:不重启服务的情况下替换类。
-
-
层级位置:通常挂载在 Application ClassLoader 之下。
四、 破坏双亲委派:Tomcat 的"叛逆"
虽然双亲委派是标准推荐的规则,但在某些特定场景下,我们必须打破它。最经典的案例就是 Tomcat。
1. 为什么要破坏?
双亲委派有一个致命的"缺点":它默认全类名(包名+类名)一样,就是同一个类。
但在 Tomcat 这种 Web 容器里,这行不通。
-
场景:
-
你的 WebApp A 依赖
Spring 4.0。 -
你的 WebApp B 依赖
Spring 5.0。 -
这两个 jar 包里的类名都叫
org.springframework.context.ApplicationContext。
-
-
冲突: 如果遵循双亲委派,AppClassLoader 加载了 Spring 4 的类,那么 App B 想要用 Spring 5 的时候,加载器一看名字一样,直接把 4 的给它了,App B 瞬间报错。
- 需求 :我们需要 类隔离。
2. 如何破坏?
在 Java 代码中,双亲委派的逻辑写在 java.lang.ClassLoader 的 loadClass() 方法中:
原生逻辑:先检查
loaded->parent.loadClass()->findClass()。
要破坏双亲委派,只需要自定义类加载器,重写 loadClass() 方法,强制让他从0层转移到以-1层作为起点。把"先找父类"的逻辑去掉,改成"先找自己"。

3. Tomcat 的解决方案
Tomcat 为每个 Web 应用创建了一个独立的 WebappClassLoader。
-
WebappClassLoader (隔离):
-
针对
/WEB-INF/classes和/WEB-INF/lib下的类。 -
破坏规则 :加载请求来了,先自己尝试加载。自己找不到,再遵循双亲委派去找父加载器(加载 JDK 核心类)。
-
结果:WebApp A 用自己的加载器加载 Spring 4,WebApp B 用自己的加载器加载 Spring 5。JVM 认为它们是不同的类,互不干扰。
-
-
SharedClassLoader (共享):
-
如果有多个应用版本一致(比如都用 Spring 5),为了节省内存,避免重复加载。
-
Tomcat 提供了
SharedClassLoader,让它去加载指定目录下的共享 jar 包。这一部分是遵循双亲委派的。
-
四、 总结
-
类加载器 负责将字节码搬运到内存。
-
双亲委派 是"向上委托,向下加载"的机制,核心目的是安全 和防重复。
-
破坏双亲委派 并不全是坏事,在需要类隔离 (如 Tomcat)或热部署 的场景下,我们需要通过自定义类加载器重写
loadClass来实现"优先加载自己"。
理解了这些,你就真正看懂了 Java 类加载的本质。