「Java 进阶之路」系列 Day32
写在前面
双亲委派模型是JVM类加载机制里最常被问到的知识点,很多人能背出"先委派给父加载器,父加载器加载不了子加载器才自己加载"这句话,但问一句"这么设计到底图什么",往往说不到点子上。这篇从一个具体问题------"自己写一个java.lang.String类会发生什么"------切入,把这套机制讲透。
一、是什么:一层一层往上委托的加载顺序
JVM里的类加载器分成几层,从上到下依次是:
- 启动类加载器(Bootstrap ClassLoader) :JVM自身的一部分,用C++实现,负责加载核心类库(比如
java.lang.String所在的java.base模块) - 平台类加载器/扩展类加载器(Platform/Extension ClassLoader):负责加载一些扩展类库
- 应用程序类加载器(Application ClassLoader):负责加载你自己写的类、以及classpath下引入的第三方依赖,日常写代码接触最多的就是它
- 自定义类加载器:开发者根据需要自己实现的类加载器
双亲委派模型 规定:一个类加载器收到加载某个类的请求时,不会自己先尝试加载,而是先把这个请求委派给自己的父加载器,父加载器也是同样的逻辑、继续往上委派,一路委派到最顶层的启动类加载器。只有当上层的加载器都表示"我加载不了这个类"(在自己负责的范围内找不到),请求才会一层一层退回来,最终由发起请求的那个加载器自己尝试加载。
二、为什么要这样设计:安全性 + 避免重复加载
回到开头的问题:自己写一个 java.lang.String 会怎样
java
package java.lang; // 故意放进java.lang包下
public class String {
public String() {
System.out.println("这是我自己写的String");
}
}
如果没有双亲委派,这个自定义的String类完全有可能在某个场景下被加载、替换掉JDK原本的java.lang.String,这是极其危险的------String几乎是整个Java体系的地基,一旦能被随意替换,攻击者理论上可以伪造一个"看起来一样但内部逻辑被篡改"的核心类,做出各种破坏性的事情。
双亲委派模型从机制上杜绝了这种风险 :不管这个自定义String类是从哪个加载器发起的加载请求,按照双亲委派的规则,请求都会一路网上委派到启动类加载器;启动类加载器在自己负责的核心类库(java.base模块)里,本来就能找到java.lang.String这个类,直接加载JDK自己的这一份并返回------请求根本不会有机会退回到发起请求的那个加载器手里,你自己写的那个String类永远没有被加载的机会 。这就是双亲委派带来的第一个核心价值:保护核心类库不被篡改,也被称为"沙箱安全机制"。
第二个价值:避免同一个类被重复加载
双亲委派还保证了同一个类在JVM里始终由同一个、且是最合适的那个类加载器去加载,不会出现同一个类被多个加载器各自加载一份、造成资源浪费甚至类型不一致 的问题(如果同一个类被两个不同的类加载器分别加载,JVM会认为这是两个不同的类型,即使字节码内容完全一样,这会导致很多诡异的ClassCastException)。委派机制保证每一层都先问一遍"上面有没有人已经加载过/能加载这个类",避免了各个层级各自为战、重复劳动。
三、怎么用:双亲委派也有被主动打破的场景
双亲委派模型是JDK推荐的一种实现方式 ,不是Java语言规范强制要求的,实际上有些场景会主动"打破"它,最典型的是SPI机制(Service Provider Interface)。
以JDBC为例:java.sql.Driver这个接口定义在核心类库里,由启动类加载器负责加载;但具体的数据库驱动实现(比如MySQL的驱动类)是第三方jar包,只能由应用程序类加载器(或者更下层的加载器)加载。问题来了------启动类加载器只负责加载"接口"这一层,它压根不知道、也没有能力去加载 这些下层加载器负责的具体驱动实现类。如果严格按照双亲委派"只能往上委托、上层加载器不能反向使用下层加载器"的逻辑,DriverManager(由启动类加载器加载)就没办法加载到具体的MySQL驱动类。
解决办法是"线程上下文类加载器"(Thread Context ClassLoader) :JVM允许在线程上下文里手动指定一个类加载器(通常是应用程序类加载器),当DriverManager这类由上层加载器加载的代码需要用到下层加载器才能找到的类时,可以主动去获取这个线程上下文类加载器、用它来完成加载,从而"反向"绕过了双亲委派原本的单向委托方向。类似的场景还有Tomcat这类Web容器,为了实现不同Web应用之间类的相互隔离(不同war包可能引入了同名但版本不同的类库),也自定义了类加载器、局部调整了双亲委派的严格顺序。
这也说明双亲委派不是绝对不可违背的铁律,而是一种在大多数场景下都合理、但遇到"需要跨层级访问"这类特殊需求时,可以有针对性地被打破的默认约定。
四、面试追问
Q1:什么是双亲委派模型?
类加载器收到加载某个类的请求时,不会自己先尝试加载,而是先委派给父加载器,父加载器继续向上委派,一直委派到最顶层的启动类加载器;只有当上层加载器都无法加载这个类时,请求才会逐层退回,最终由最初发起请求的加载器自己完成加载。
Q2:双亲委派模型解决了什么问题?
主要解决两个问题:一是安全性,保证核心类库(比如java.lang.String)不会被自定义的同名类替换,因为请求最终一定会被最上层的启动类加载器截获、优先加载官方版本,构成"沙箱安全机制";二是避免同一个类被不同类加载器重复加载,保证类型的唯一性和一致性,避免出现看似相同实际类型却不一致导致的ClassCastException。
Q3:为什么自己定义一个 java.lang.String 类,永远不会替换掉 JDK 自带的?
因为不管这个自定义类是从哪一层类加载器发起加载请求的,按照双亲委派规则都会一路向上委派到启动类加载器,而启动类加载器在自己负责的核心类库范围内本来就能找到java.lang.String,会直接加载并返回这个官方版本,请求根本不会有机会退回到下层去加载自定义的那份,从机制上杜绝了核心类被篡改的可能。
Q4:双亲委派模型是Java语言规范强制要求的吗?
不是,它是JDK推荐的一种类加载器实现方式,不是语言规范强制约束。实际场景中有些需求会主动打破这个模型,最典型的是JDBC这类SPI机制------由启动类加载器加载的接口需要用到应用程序类加载器才能加载的具体实现类,这种情况通过线程上下文类加载器反向绕过了双亲委派原本的单向委托方向。
Q5:为什么 JDBC 的 DriverManager 需要打破双亲委派?
因为java.sql.Driver接口由启动类加载器加载,但具体的数据库驱动实现类是第三方jar包,只能由应用程序类加载器加载。启动类加载器只负责它自己那部分核心类库,没有能力也不会去加载这些下层加载器负责的实现类,如果严格遵循双亲委派单向委托的规则,DriverManager就无法找到具体的驱动实现。所以JDK引入了线程上下文类加载器,让由上层加载器加载的代码能够反向借用下层加载器去完成加载,绕开了这个限制。
下一篇预告
Day33 讲垃圾回收算法------标记清除、标记整理、复制算法这三种基础算法各自的原理和取舍,是理解后面各种GC器具体实现的基础。