你确定你真的理解 _ 双亲委派 _ 了吗?!,阿里内部核心 Java 进阶手册
这就不得不提到"双亲委派机制"。
首先,我们需要知道的是,Java 语言系统中支持以下 4 种类加载器:
Bootstrap ClassLoader 启动类加载器
Extention ClassLoader 标准扩展类加载器
Application ClassLoader 应用类加载器
User ClassLoader 用户自定义类加载器
这四种类加载器之间,是存在着一种层次关系的,如下图
[图片上传中...(image-27cd3a-1611066134740-1)]
?
一般认为上一层加载器是下一层加载器的父加载器,那么,除了 BootstrapClassLoader 之外,所有的加载器都是有父加载器的。
那么,所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
那么,什么情况下父加载器会无法加载某一个类呢?
其实,Java 中提供的这四种类型的加载器,是有各自的职责的:
Bootstrap ClassLoader ,主要负责加载 Java 核心类库,%JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class 等。
Extention ClassLoader,主要负责加载目录 %JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件。
Application ClassLoader ,主要负责加载当前应用的 classpath 下的所有类
User ClassLoader , 用户自定义的类加载器,可加载指定路径的 class 文件
那么也就是说,一个用户自定义的类,如 com.hollis.ClassHollis 是无论如何也不会被 Bootstrap 和 Extention 加载器加载的。
为什么需要双亲委派?
如上面我们提到的,因为类加载器之间有严格的层次关系,那么也就使得 Java 类也随之具备了层次关系。
或者说这种层次关系是优先级。
比如一个定义在 java.lang 包下的类,因为它被存放在 rt.jar 之中,所以在被加载过程汇总,会被一直委托到 Bootstrap ClassLoader,最终由 Bootstrap ClassLoader 所加载。
而一个用户自定义的 com.hollis.ClassHollis 类,他也会被一直委托到 Bootstrap ClassLoader,但是因为 Bootstrap ClassLoader 不负责加载该类,那么会在由 Extention ClassLoader 尝试加载,而 Extention ClassLoader 也不负责这个类的加载,最终才会被 Application ClassLoader 加载。
这种机制有几个好处。
首先,通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
另外,通过双亲委派的方式,还保证了安全性。因为 Bootstrap ClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的 JDK。
那么,就可以避免有人自定义一个有破坏功能的 java.lang.Integer 被加载。这样可以有效的防止核心 Java API 被篡改。
"父子加载器"之间的关系是继承吗?
很多人看到父加载器、子加载器这样的名字,就会认为 Java 中的类加载器之间存在着继承关系。
甚至网上很多文章也会有类似的错误观点。
这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
如下为 ClassLoader 中父加载器的定义:
public abstract class ClassLoader {// The parent class loader for delegationprivate final ClassLoader parent;}复制代码
双亲委派是怎么实现的?
双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现并不复杂。
实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass()方法之中:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException t
hrown if class not found// from the non-null parent class loader}
if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);
// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}复制代码
代码不难理解,主要就是以下几个步骤:
1、先检查类是否已经被加载过 2、若没有加载则调用父加载器的 loadClass()方法进行加载 3、若父加载器为空则默认使用启动类加载器作为父加载器。 4、如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass()方法进行加载。
如何主动破坏双亲委派机制?
知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。
因为他的双亲委派过程都是在 loadClass 方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的 loadClass 方法,使其不进行双亲委派即可。
loadClass()、findClass()、defineClass()区别
ClassLoader 中和类加载有关的方法有很多,前面提到了 loadClass,除此之外,还有 findClass 和 defineClass 等,那么这几个方法有什么区别呢?
loadClass()
就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
findClass()
根据名称或位置加载.class 字节码
definclass()
把字节码转化为 Class
这里面需要展开讲一下 loadClass 和 findClass,我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写 loadClass 方法。
那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?
这时候,就可以继承 ClassLoader,并且重写 findClass 方法。findClass()方法是 JDK1.2 之后的 ClassLoader 新添加的一个方法。
/**
@since 1.2*/protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}复制代码
这个方法只抛出了一个异常,没有默认实现。
JDK1.2 之后已不再提倡用户直接覆盖 loadClass()方法,而是建议把自己的类加载逻辑实现到 findClass()方法中。
因为在 loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的 findClass()方法来完成加载。
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承 ClassLoader,并且在 findClass 中实现你自己的加载逻辑即可。
双亲委派被破坏的例子
双亲委派机制的破坏不是什么稀奇的事情,很多框架、容器等都会破坏这种机制来实现某些功能。
第一种被破坏的情况是在双亲委派出现之前。
由于双亲委派模型是在 JDK1.2 之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。
第二种,是 JNDI、JDBC 等需要加载 SPI 接口实现类的情况。
**第三种是为了实现热插拔热部署工具。**为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。
第四种时 tomcat 等 web 容器的出现。
第五种时 OSGI、Jigsaw 等模块化技术的应用。
为什么 JNDI,JDBC 等需要破坏双亲委派?
我们日常开发中,大多数时候会通过 API 的方式调用 Java 提供的那些基础类,这些基础类时被 Bootstrap 加载的。
但是,调用方式除了 API 之外,还有一种 SPI 的方式。
如典型的 JDBC 服务,我们通常通过以下方式创建数据库连接:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");复制代码
在以上代码执行之前,DriverManager 会先被类加载器加载,因为 java.sql.DriverManager 类是位于 rt.jar 下面的 ,所以他会被根加载器加载。
类加载时,会执行该类的静态方法。其中有一段关键的代码是:
评论