【JVM】双亲委派

你这份总结整体是对的,核心就是围绕三个问题:

类加载器是谁?它负责把 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 中出现多个"看起来同名,但实际上不同"的类。

双亲委派可以减少重复加载,保证核心类、公共类优先由上层加载器统一加载。


六、双亲委派的执行逻辑

它主要体现在 ClassLoaderloadClass() 方法里。

简化逻辑如下:

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 核心类;第二是唯一性,避免同一个类被多个类加载器重复加载。

双亲委派主要是在 ClassLoaderloadClass() 方法中实现的。如果想破坏双亲委派,可以自定义类加载器并重写 loadClass() 方法,改成优先自己加载。

典型场景是 Tomcat。因为一个 Tomcat 里可以部署多个 Web 应用,不同应用可能依赖同一个类库的不同版本。如果完全遵循双亲委派,就无法实现多版本隔离。因此 Tomcat 为每个 Web 应用提供独立的 WebappClassLoader,让 Web 应用自己的类优先由自己的类加载器加载。这样即使类的全限定名相同,只要类加载器不同,JVM 也认为它们是不同的类,从而实现类隔离和多版本共存。同时,Tomcat 也提供 SharedClassLoader 来加载公共 jar,避免多个应用重复加载相同依赖。

相关推荐
ourenjiang1 小时前
【测试框架Junit】强制终止JVM进程
jvm·junit
Full Stack Developme3 小时前
G1回收器的工作机制
java·jvm
填满你的记忆3 小时前
JVM 面试题 Top40
jvm·面试题
故渊at3 小时前
第二板块:Android 四大组件标准化学理 | 第十篇:ContentProvider 数据共享与 SQLite 引擎
android·jvm·数据库·sqlite·contentprovider
骄马之死3 小时前
JVM 核心知识
java·jvm
Java面试题总结3 小时前
采集网关的离线缓存与断点续传——当网络不可靠时,数据一条都不能丢
网络·jvm·缓存
J-Tony1114 小时前
【JVM】编译&&解释
jvm
J-Tony1117 小时前
【JVM】JVM调优经验
jvm·测试工具
weixin_5231853219 小时前
Java基础知识总结(二):JVM内存结构与变量生命周期
java·开发语言·jvm