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