Java 开发 6 年了,你确定你真的理解 _ 双亲委派 _ 了吗?
如下为 ClassLoader 中父加载器的定义:
双亲委派是怎么实现的?
双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现并不复杂。
实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass()方法之中:
代码不难理解,主要就是以下几个步骤:
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 新添加的一个方法。
这个方法只抛出了一个异常,没有默认实现。
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 服务,我们通常通过以下方式创建数据库连接:
在以上代码执行之前,DriverManager 会先被类加载器加载,因为 java.sql.DriverManager 类是位于 rt.jar 下面的 ,所以他会被根加载器加载。
类加载时,会执行该类的静态方法。其中有一段关键的代码是:
这段代码,会尝试加载 classpath 下面的所有实现了 Driver 接口的实现类。
那么,问题就来了。
DriverManager 是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有 Driver 的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。
那么,怎么解决这个问题呢?
于是,就在 JDBC 中通过引入 ThreadContextClassLoader(线程上下文加载器,默认情况下是 AppClassLoader)的方式破坏了双亲委派原则。
我们深入到 ServiceLoader.load 方法就可以看到:
第一行,获取当前线程的线程上下?类加载器 AppClassLoader,?于加载 classpath 中的具体实现类。
为什么 Tomcat 要破坏双亲委派
我们知道,Tomcat 是 web 容器,那么一个 web 容器可能需要部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。
如多个应用都要依赖 hollis.jar,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class。
如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
所以,Tomcat 破坏双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。
Tomcat 的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交给 CommonClassLoader 加载,这和双亲委派刚好相反。
模块化技术与类加载机制
近几年模块化技术已经很成熟了,在 JDK 9 中已经应用了模块化的技术。
其实早在 JDK 9 之前,OSGI 这种框架已经是模块化的了,而 OSGI 之所以能够实现模块热插拔和模块内部可见性的精准控制都归结于其特殊的类加载机制,加载器之间的关系不再是双亲委派模型的树状结构,而是发展成复杂的网状结构。
[图片上传中...(image-257470-1611066134739-0)]
?
在 JDK 中,双亲委派也不是绝对的了。
在 JDK9 之前,JVM 的基础类以前都是在 rt.jar 这个包里,这个包也是 JRE 运行的基石。
感受:
其实我投简历的时候,都不太敢投递阿里。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。
特别感谢一面的面试官捞了我,给了我机会,同时也认可我的努力和态度。对比我的面经和其他大佬的面经,自己真的是运气好。别人 8 成实力,我可能 8 成运气。所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。希望自己能继续保持学习的热情,继续努力走下去。
也祝愿各位同学,都能找到自己心动的 offer。
分享我在这次面试前所做的准备(刷题复习资料以及一些大佬们的学习笔记和学习路线),都已经整理成了电子文档,需要的朋友可以【点赞+关注】戳这里即可免费获取
评论