最直接的大白话让你彻底搞懂Java类加载器中的双亲委派模型
关于作者我是星河一个深耕自己不内耗的长期主义者。一个对技术充满激情对工作对生活充满热情的热血青年。坚信真正能让大家看懂的技术文章才是好文章坚持用更通俗易懂的大白话写技术博文并会持续更新。我是星河愿我们既能凝望脚下的路也能心怀璀璨的远方。和之前一样今天还是用通俗易懂的大白话来写点我自己的理解和总结。跟着我的思路绝对给你搞搞的明明白白的首先别感觉很难看我写的一点都不难。经常看我文章的应该都能感觉到我写的文章和你看的其他讲技术的文章可能不一样我写的全都是大白话来写的没什么生硬的专业术语而且都是循序渐进来写的。我会把我是怎么从不理解不明白到最后我是怎么搞明白的这个脉络我会尽可能的用通俗易懂的大白话记录下来。我相信我自己是这么理解过来的那大多数人按照我的脉络来看肯定也都能看明白。希望能尽自己的一点绵薄之力来帮更多人理解这些枯燥难懂的Java知识。ok直接开始今天的内容。今天来讲一下Java类加载器中的双亲委派。在说双亲委派模型之前首先得先了解一下类加载阶段。一、类的加载阶段类加载阶段分为加载、连接、初始化三个阶段而加载阶段需要通过类的全限定名来获取定义了此类的二进制字节流。Java特意把这一步抽出来用类加载器来实现。下面具体讲一下默认的三种类加载器二、类加载器Java自身给咱们提供了3种类加载器从上到下依次是1、启动类加载器(Bootstrap ClassLoader)它是属于虚拟机自身的一部分用C实现的主要负责加载JAVA_HOME\jre\lib目录中或被-Xbootclasspath指定的路径中的字节码文件。他负责加载的是jdk类库里的核心类库。它是最顶层的类加载器等于是所有类加载器最顶层的父类。大白话说他管的地盘就是JAVA_HOME\jre\lib目录2、扩展类加载器(Extension ClassLoader)或平台类加载器它是Java实现的独立于虚拟机主要负责加载JAVA_HOME\jre\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。他负责加载的也是jdk类库里的字节码但不是核心类库。大白话说它管的地盘就是JAVA_HOME\jre\lib\ext目录。然后还有一点需要知道的是JDK9之后JDK的源码是做了模块化的处理然后从JDK9之后扩展类加载器变成了平台类加载器。虽然模块化了但是它还是有自己管理的地盘的只不过JDK9改成平台类加载器后它的地盘从能看得到的 JAVA_HOME\jre\lib\ext目录 改成了逻辑上对应的JDK里的模块而已。3、应用程序类加载器(Application ClassLoader)它是Java实现的独立于虚拟机。主要负责加载咱们程序员写的代码对应的字节码文件也就是咱们项目classPath下的字节码文件以及咱们项目里引用的第三方jar包里的字节码文件。大白话说它管的地盘就是程序员写的代码和引入的第三方jar。然后还需要知道的是应用程序类加载器是我们程序中默认的类加载器。我对类加载大白话的理解总结非常重要类加载器的概念说完了之后可能小伙伴们觉得上边这些和书上写的那些死板的概念差不多还是不能彻底理解。所以我再用最通俗易懂的大白话来给大家描述一下我的理解其实类加载器你彻底的大白话理解吃透了之后就非常简单其实说白了就是java里默认提供了三种类加载器每种类加载都有自己管理的地盘儿启动类加载器(Bootstrap ClassLoader)管理的地盘是JAVA_HOME\jre\lib下的类如果需要加载的类在JAVA_HOME\jre\lib 下放着呢启动类加载器就去加载这个类。扩展类加载器(Extension ClassLoader)或平台类加载器管理的地盘是JAVA_HOME\jre\lib\ext下的类如果需要加载的类在 JAVA_HOME\jre\lib\ext 下放着呢扩展类加载器就去加载这个类。应用程序类加载器(Application ClassLoader)管理的地盘是咱们写的类和咱们引入的第三方的jar里的类如果咱们写的类和咱们引入的第三方的jar里的类需要加载时那应用程序类加载器就去加载这些类。以上大白话的理解非常重要因为接下来讲到的双亲委派执行类加载的过程中会用到所以一定要真正大白话理解了。再说一遍其实最核心就一句话每种类加载器都有各自管理的地盘儿范围是哪个类加载器地盘里的类就由哪个类加载器来加载。就这么简单就记住这一句就完了。三、双亲委派模型跟着我的思路真正理解了上面这些后就能来看看双亲委派模型了。先看一个简略的描述图根据图片大致知道了类加载器之间的关系之后再来聊一聊双亲委派双亲委派的意思是当加载一个类时这个类加载的请求默认最先到达最底层的类加载器也就是应用程序类加载器。但是它接收到一个类的加载请求时它不会自己立马就去尝试加载这个类它会先把这个加载类的请求向上委托给自己的父类加载器。然后如果父类还有父类就再继续向上一直递归到最顶层。然后最顶层的类加载器会去自己管辖的范围内找这个类找到的话就找到了没找到的话它会让下边的子类加载器去尝试加载向下也是依次递归。其实说白就是接收到一个类的加载请求时先自下而上再自上而下更大白话的来说就是老三先接到类加载的请求它会先委托给老二老二继续给老大。然后老大先在自己的地盘里看能不能找到这个类如果找到了他就直接加载了就结束了。如果老大在自己地盘里没找到那就交给老二老二去自己地盘里找如果找到了就加载。如果老二在自己地盘里没找到那就给老三老三在自己地盘里找到了就加载找不到就抛类找不到的异常。双亲委派大白话来说就这么简单没什么难理解的也没什么神秘的。以前你觉得难那是因为没人真正的给你讲明白。看完我写的你肯定就真正理解了。还有需要知道的是“双亲委派”里的双亲其实就仅仅指的是父类没有mother只是最初的概念里使用了 parents 这个单词来描述所以国内的资料也都跟着翻译成了“双亲父母”但其实本意想表达的是类加载之间的上下级关系而已。而且这里的父类也不是我们Java里平时所说的那种继承关系只是类加载之间上下的调用逻辑是这样而已。下图是双亲委派模型的实现摘抄自《深入理解Java虚拟机》从这段源码里可以清晰的看到类加载器之间的调用层级关系。如果对类加载器想更深入的了解的话可以看我写的另外一篇Java中如何去自定义一个类加载器四、双亲委派的好处知道了什么是双亲委派后下面再来说一说搞这么麻烦的一套流程是为了啥有啥好处首先要知道双亲委派它不是一种强制性的约束只是JVM规范里建议使用的一种模型。也就是说你不这么做也不会报错但是但是它是Java的设计者默认使用并且推荐使用的类加载器的方式。那双亲委派有啥好处呢其中最重要的一点就是这个先自下而上再自上而下的机制很好的保证了JDK核心类的加载。啥意思很多小伙伴会问就这就保护了JDK底层的核心类的加载好不信的话我下面举例子来说就拿 java.lang.Object 这个类来说假如有个人他也写一个 java.lang.Object这个类包名类名都一模一样然后他在里边写了一些危险的代码然后他想加载运行这些代码。但是你想想由于jvm底层使用了 先自下而上再自上而下的 双亲委派的模式那他伪造的 java.lang.Object这个类 根本就没机会被jvm加载更别说运行了。为什么你想想首先最底层的老三应用程序类加载器先接到 java.lang.Object 这个类的类加载请求但是它向上递归给了最顶层的老大启动类加载器老大一看哦你是想加载 java.lang.Object 这个类啊那行我来我地盘里JAVA_HOME\jre\lib看能不能找到这个类然后呢然后老大就真的在它的地盘JAVA_HOME\jre\lib下的rt.jar里找到 java.lang.Object 这个类了然后jdk里的JAVA_HOME\jre\lib下的 java.lang.Object 类就被加载进JVM里了你伪造的这个 java.lang.Object类你随便伪造但是加载运行时jvm理都不会理你的你伪造的根本没机会被加载更别说运行了。现在懂了吧因为这个机制使得系统中只会有一个 java.lang.Object而且是jdk底层自带的那个最正统的Object因此既然推荐使用这种模型那当然是有道理的。jdk底层的 java.lang.Object 类这样被保护了jdk里其他底层的类也都是这样被保护起来了。五、打破双亲委派但是人生不如意事十之八九有些情况下不得不违反这个约束例如JDBC。在JDBC4.0以后开始支持使用spi的方式来注册这个Driver具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个然后使用的时候就不需要我们手动的去加载驱动了我们只需要直接获取连接就可以了。具体的spi的实现过程可以去查询其他文章我只做简单分析1.扫描所有的jar包去找META-INF/services/java.sql.Driver文件中获取具体的实现类名2.利用Class.forName()去加载该类那么问题就来了Class.forName()加载用的是调用者的Classloader这个调用者DriverManager是在rt.jar中的是由启动类加载器进行加载的而com.mysql.jdbc.Driver肯定不在JAVA_HOME/jre/lib下(可以通过System.getProperty(sun.boot.class.path)获取启动类加载器加载的路径)所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了父级加载器无法加载子级类加载器路径中的类。你先得知道SPI(Service Provider Interface)这玩意和API不一样它是面向拓展的也就是我定义了这个SPI具体如何实现由扩展者实现。我就是定了个规矩。JDBC就是如此在rt.jar里面定义了这个SPImysql有mysql的jdbc实现oracle有oracle的jdbc实现反正我java不管你内部如何实现的反正你们都得统一按我这个来这样我们java开发者才能容易的调用数据库操作。所以因为这样那就不得不违反这个约束啊Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。因为它的手只能摸到JAVA_HOME\jre\lib中其他的它无能为力。这就违反了自下而上的委托机制了。Java就搞了个线程上下文类加载器有了线程上下文类加载器父类加载器就可以请求子类加载器去完成类加载的动作这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器实际上已经违背了双亲委派模型的一般性原则但这也是无可奈何的事情。如果想更深入了解关于SPI的内容可以看我之前写的这一篇Java SPI概念、实现原理、优缺点、应用场景、使用步骤、实战SPI案例纯手敲 原创不易如果这篇文章对你有所启发或帮助希望可以花费你一秒钟的时间点亮【赞和推荐】如果能点【分享】给更多同行的人那就更好了。你的每一个互动都是我持续创作的最大动力。感恩遇见感谢陪伴。点击下方 微信公众号 获取更多Java干货