你这份总结整体是对的,核心就是围绕三个问题:
类加载器是谁?它负责把 class 文件加载进 JVM。
双亲委派是什么?它规定类加载器加载类时,先让父加载器尝试加载。
为什么要破坏双亲委派?因为有些场景需要类隔离、多版本共存。
一、什么是类加载器?
类加载器可以理解成:
JVM 中负责把
.class字节码文件加载到内存中的组件。
比如你写了一个类:
java
public class UserService {
}
编译后会生成:
java
UserService.class
程序运行时,JVM 不会一开始就把所有 class 文件都加载进来,而是用到哪个类,就由类加载器把哪个类加载进 JVM 内存。
加载进去之后,JVM 才能创建对象、调用方法、执行代码。
二、类加载器有哪些?
常见有四类。
1. Bootstrap ClassLoader:启动类加载器
它是最顶层的类加载器。
主要加载 Java 最核心的类,比如:
java
java.lang.String
java.lang.Object
java.util.ArrayList
这些类来自 JDK 的核心类库。
在 Java 8 里,主要加载:
text
JAVA_HOME/jre/lib
下面的核心类库。
它比较特殊,不是 Java 写的,而是 C/C++ 实现的,所以我们在 Java 代码中一般看不到它。
例如:
java
System.out.println(String.class.getClassLoader());
输出通常是:
java
null
这个 null 不是说没有类加载器,而是表示它由 Bootstrap ClassLoader 加载。
2. Extension ClassLoader:扩展类加载器
它负责加载 JDK 扩展目录下的类。
在 Java 8 中,主要是:
text
JAVA_HOME/jre/lib/ext
它的父加载器是 Bootstrap ClassLoader。
不过需要注意,Java 9 以后引入了模块化机制,Extension ClassLoader 已经被 Platform ClassLoader 替代了。
面试中如果讲 Java 8 的类加载器体系,继续说 Extension ClassLoader 是可以的。
3. Application ClassLoader:应用程序类加载器
这个最常见。
它负责加载我们自己写的业务代码,以及项目依赖的 jar 包。
也就是 classpath 路径下的类,比如:
text
target/classes
lib/*.jar
平时 Spring Boot、普通 Java 项目里面的大部分类,都是它加载的。
它的父加载器是 Extension ClassLoader,也就是:
text
Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader
4. 自定义类加载器
我们也可以自己写一个类加载器,继承 ClassLoader。
常见用途有:
text
1. 加载指定路径下的 class 文件
2. 实现类隔离
3. 实现热部署
4. 实现插件化
5. 实现同一个类的多版本共存
6. 破坏双亲委派
比如 Tomcat、Dubbo、OSGi、一些热部署框架,都会用到自定义类加载器。
三、什么是双亲委派?
双亲委派说的是:
一个类加载器收到类加载请求后,自己不会立刻加载,而是先交给父类加载器去加载。父加载器加载不了,才轮到自己加载。
加载流程大概是:
text
Application ClassLoader
|
v
Extension ClassLoader
|
v
Bootstrap ClassLoader
假设现在要加载一个类:
java
java.lang.String
流程是:
text
1. Application ClassLoader 收到加载请求
2. 它不先自己加载,而是交给 Extension ClassLoader
3. Extension ClassLoader 继续交给 Bootstrap ClassLoader
4. Bootstrap ClassLoader 发现 String 是核心类,自己加载
5. 加载成功,返回结果
所以最终 String 是由 Bootstrap ClassLoader 加载的。
四、双亲委派不是继承关系
你这句话很重要:
类加载器的父子关系不是继承,而是组合。
什么意思?
不是说:
java
ApplicationClassLoader extends ExtensionClassLoader
而是说每个 ClassLoader 内部有一个 parent 字段,指向自己的父加载器。
大概类似:
java
class ClassLoader {
private final ClassLoader parent;
}
所以这个"父加载器"是逻辑上的父子关系,不是 Java 类继承上的父子关系。
五、为什么要有双亲委派?
主要有两个好处:安全性 和唯一性。
1. 保证核心类库安全
假设没有双亲委派,我们自己写一个类:
java
package java.lang;
public class String {
}
如果 JVM 优先加载我们自己写的 java.lang.String,那就很危险了。
Java 核心类库可能被用户随便替换,比如:
java
java.lang.String
java.lang.Object
java.util.HashMap
这会导致整个 Java 运行环境混乱。
有了双亲委派之后,加载 java.lang.String 时,会先交给 Bootstrap ClassLoader。
Bootstrap ClassLoader 发现自己能加载,于是直接加载 JDK 自带的 String,不会加载你自己写的那个。
所以双亲委派可以防止用户自定义类冒充核心类。
2. 保证类的唯一性
在 JVM 中,判断两个类是否相同,不只看类的全限定名,还要看加载它的类加载器。
也就是说,一个类的唯一标识是:
text
类加载器 + 类的全限定名
例如:
text
com.example.User
如果被同一个类加载器加载一次,那么 JVM 认为它就是同一个类。
如果没有双亲委派,可能多个类加载器都去加载同一个类,导致 JVM 中出现多个"看起来同名,但实际上不同"的类。
双亲委派可以减少重复加载,保证核心类、公共类优先由上层加载器统一加载。
六、双亲委派的执行逻辑
它主要体现在 ClassLoader 的 loadClass() 方法里。
简化逻辑如下:
java
protected Class<?> loadClass(String name, boolean resolve) {
// 1. 先检查这个类是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 2. 交给父加载器加载
c = parent.loadClass(name, false);
} else {
// 3. 如果没有父加载器,就交给 Bootstrap 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败
}
if (c == null) {
// 4. 父加载器加载不了,才自己加载
c = findClass(name);
}
}
return c;
}
核心顺序就是:
text
先查是否已加载
再交给父加载器
父加载器不行,自己再加载
七、如何破坏双亲委派?
正常情况下,如果你自定义类加载器,一般只需要重写:
java
findClass()
因为 loadClass() 仍然保留双亲委派逻辑。
如果你想破坏双亲委派,就要重写:
java
loadClass()
然后把加载顺序改成:
text
先自己加载
自己加载不了,再交给父加载器
这就打破了原来的:
text
先父后子
变成了:
text
先子后父
这就是破坏双亲委派。
八、为什么 Tomcat 要破坏双亲委派?
这是重点。
一个 Tomcat 可以部署多个 Web 应用,比如:
text
Tomcat
├── app1
├── app2
└── app3
假设:
text
app1 使用 spring-5.0.jar
app2 使用 spring-6.0.jar
但是它们里面的类名可能一样,比如:
text
org.springframework.context.ApplicationContext
如果完全遵循双亲委派,那么公共父加载器一旦加载了某个版本的 Spring,其他应用就只能用这个版本。
这样就会出现问题:
text
app1 想用 Spring 5
app2 想用 Spring 6
但父加载器只加载了一份 Spring
这就无法实现多应用之间的依赖隔离。
所以 Tomcat 为每个 Web 应用创建一个独立的类加载器:
text
app1 -> WebappClassLoader1
app2 -> WebappClassLoader2
app3 -> WebappClassLoader3
这样即使类名完全一样:
text
org.springframework.context.ApplicationContext
只要它们是被不同的类加载器加载的,JVM 就认为它们是不同的类。
也就是:
text
WebappClassLoader1 + org.springframework.context.ApplicationContext
和:
text
WebappClassLoader2 + org.springframework.context.ApplicationContext
在 JVM 看来不是同一个类。
这样就实现了:
text
不同 Web 应用之间的类隔离
不同 Web 应用可以使用不同版本的依赖
九、Tomcat 是完全不遵循双亲委派吗?
不是。
Tomcat 不是简单粗暴地完全破坏双亲委派,而是有选择地破坏。
大概规则是:
text
Java 核心类:仍然交给 Bootstrap 加载
Tomcat 自己的类:由 Tomcat 的类加载器加载
Web 应用自己的类:优先由自己的 WebappClassLoader 加载
公共共享类:可以由 SharedClassLoader 加载
也就是说,Tomcat 的目标不是"反对双亲委派",而是为了实现:
text
Web 应用之间互相隔离
公共类可以共享
核心类仍然安全
十、SharedClassLoader 是干什么的?
你后面这个问题也很关键。
如果每个 Web 应用都有自己的 WebappClassLoader,那确实会有一个问题:
text
app1 有 spring.jar
app2 有 spring.jar
app3 有 spring.jar
如果它们用的是同一个版本,那每个应用都各自加载一份,就会浪费内存。
所以 Tomcat 提供了共享类加载器,比如 SharedClassLoader。
你可以把一些公共 jar 放到共享目录中,让 SharedClassLoader 统一加载。
这样多个 Web 应用都可以共享这份类。
好处是:
text
1. 避免重复加载
2. 节省内存
3. 公共依赖统一管理
但是缺点是:
text
如果不同 Web 应用需要不同版本的同一个 jar,就不能放到共享目录
否则又会回到版本冲突的问题。
十一、用一句话总结 Tomcat 的类加载机制
可以这样理解:
Tomcat 对 Web 应用自己的类采用"子加载器优先",从而实现应用隔离;对 Java 核心类仍然遵循双亲委派,保证安全;对公共 jar 可以使用 SharedClassLoader 共享,避免重复加载。
十二、面试版回答
你可以这样答:
类加载器的作用是把 class 字节码文件加载到 JVM 内存中。常见类加载器包括启动类加载器、扩展类加载器、应用程序类加载器以及自定义类加载器。启动类加载器负责加载 JDK 核心类库,扩展类加载器负责加载扩展目录下的类,应用程序类加载器负责加载 classpath 下的业务类和第三方 jar,自定义类加载器可以实现特殊的类加载逻辑。
双亲委派指的是,当一个类加载器收到类加载请求时,不会先自己加载,而是先委派给父加载器,父加载器继续向上委派,最终到启动类加载器。只有当父加载器无法加载时,子加载器才会尝试自己加载。
双亲委派的好处主要有两个:第一是安全性,防止用户自定义类覆盖 Java 核心类;第二是唯一性,避免同一个类被多个类加载器重复加载。
双亲委派主要是在 ClassLoader 的 loadClass() 方法中实现的。如果想破坏双亲委派,可以自定义类加载器并重写 loadClass() 方法,改成优先自己加载。
典型场景是 Tomcat。因为一个 Tomcat 里可以部署多个 Web 应用,不同应用可能依赖同一个类库的不同版本。如果完全遵循双亲委派,就无法实现多版本隔离。因此 Tomcat 为每个 Web 应用提供独立的 WebappClassLoader,让 Web 应用自己的类优先由自己的类加载器加载。这样即使类的全限定名相同,只要类加载器不同,JVM 也认为它们是不同的类,从而实现类隔离和多版本共存。同时,Tomcat 也提供 SharedClassLoader 来加载公共 jar,避免多个应用重复加载相同依赖。