程序与技术分享:1.2双亲委派机制及其原理

简介: 程序与技术分享:1.2双亲委派机制及其原理

1. 类加载的过程


  1.1 类加载器初始化的过程


  1.2 类加载的过程


  1.3 类的懒加载


2. jvm核心类加载器


参考博客:


一. 双亲委派机制


1.1 什么是双亲委派机制


我们先来看一个案例:


package com.lxl.jvm;


import sun.misc.Launcher;


import java.net.URL;


public class TestJDKClassLoader {


public static void main(String【】 args) {


System.out.println();


System.out.println("bootstrap Loader加载一下文件:");


URL【】 urls = Launcher.getBootstrapClassPath().getURLs();


for (int i = 0; i

System.out.println(urls【i】);


}


System.out.println();


System.out.println("extClassLoader加载以下文件");


System.out.println(System.getProperty("java.ext.dirs"));


System.out.println();


System.out.println("appClassLoader加载以下文件");


System.out.println(System.getProperty("java.class.path"));


}


}


这是打印引导类加载器, 扩展类加载器, 应用程序类加载器加载的目录.


我们来看一下:


引导类加载器加载的文件是:Launcher.getBootstrapClassPath().getURLs()下的文件


扩展类加载器加载的文件是: java.ext.dirs , java扩展类目录


应用程学类加载器, 加载的是: java.class.path , java home路径下的所有类


我们来看一下打印结果


bootstrap Loader加载一下文件:


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasign.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes


extClassLoader加载以下文件


/Users/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java


appClassLoader加载以下文件


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/deploy.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/dnsns.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jaccess.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/localedata.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/nashorn.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunec.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/zipfs.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/javaws.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfxswt.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/management-agent.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/plugin.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/ant-javafx.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/dt.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/javafx-mx.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/jconsole.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/packager.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/sa-jdi.jar:


/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/tools.jar:


/Users/Downloads/workspace/project-all/target/classes:


/Users/responsitory/org/springframework/boot/spring-boot-starter/2.2.8.RELEASE/spring-boot-starter-2.2.8.RELEASE.jar:


/Users/responsitory/org/springframework/boot/spring-boot/2.2.8.RELEASE/spring-boot-2.2.8.RELEASE.jar:


/Users/responsitory/org/springframework/spring-context/5.2.7.RELEASE/spring-context-5.2.7.RELEASE.jar:


/Users/responsitory/org/springframework/spring-aop/5.2.7.RELEASE/spring-aop-5.2.7.RELEASE.jar:


/Users/responsitory/org/springframework/spring-beans/5.2.7.RELEASE/spring-beans-5.2.7.RELEASE.jar:


/Users/responsitory/org/springframework/spring-expression/5.2.7.RELEASE/spring-expression-5.2.7.RELEASE.jar:


/Users/responsitory/org/springframework/boot/spring-boot-autoconfigure/2.2.8.RELEASE/spring-boot-autoconfigure-2.2.8.RELEASE.jar:


/Users/responsitory/org/springframework/boot/spring-boot-starter-logging/2.2.8.RELEASE/spring-boot-starter-logging-2.2.8.RELEASE.jar:


/Users/responsitory/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:


/Users/responsitory/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:


/Users/responsitory/org/apache/logging/log4j/log4j-to-slf4j/2.12.1/log4j-to-slf4j-2.12.1.jar:


/Users/responsitory/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1.jar:


/Users/responsitory/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar:


/Users/responsitory/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:


/Users/responsitory/org/springframework/spring-core/5.2.7.RELEASE/spring-core-5.2.7.RELEASE.jar:


/Users/responsitory/org/springframework/spring-jcl/5.2.7.RELEASE/spring-jcl-5.2.7.RELEASE.jar:


/Users/responsitory/org/yaml/snakeyaml/1.25/snakeyaml-1.25.jar:


/Users/responsitory/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:


/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar


通过观察,我们发现


引导类加载器,确实只加载了java home下的/jre/lib目录下面类


扩展类加载器加载了java扩展目录里面的类


但是, 应用程序类加载器, 加载的类包含了java home下/jre/lib目录, java home扩展目录下的类, 还有responsitory仓库下的类, 还有idea的类, 还有就是我们的类路径下target的类.


问题来了, 为什么AppClassLoader加载器加载了引导类加载器和扩展类加载器要加载的类呢? 这样加载不是重复了么?


其实, 不会重复加载, appClassLoader主要加载的类就是target目录下的类, 其他目录下的类基本上不会加载. 为什么呢? 这就是下面要说的双亲委派机制.


上面这个图就是双亲委派机制的图. 什么意思呢?


比如: 我现在有一个自定义的java.lxl.jvm.Math类. 首先是由应用程序类加载器去加载java.lxl.jvm.Math类, 他要去看他已经加载的类中是否有这个类, 如果有, 就直接返回回来, 如果没有, 就委托扩展类加载器去加载. 扩展类加载器去查看已经加载的类是否有java.lxl.jvm.Math, 如果有就返回,如果没有就继续委托它的父类引导类加载器去加载. 这时候, 我们都知道, Math类是我自己定义的, 引导类加载器中不可能有, 所以, 他就会让扩展类加载器去加载, 扩展类加载器中有没有呢? 当然也没有, 于是委托应用程序类加载器, ok,应用程序类加载器是有的, 于是就可以加载, 然后返回了.


那么, 这里有一个问题, 那就是, 由应用程序类加载器首先加载, 然后最后又回到了应用程序类加载器. 绕了一圈又回来了, 这样是不是有些多此一举呢, 循环了两次? 为什么一定要从应用程序类加载器加载呢? 直接从引导类加载器加载不好么?只循环一次啊....


其实, 对于我们的项目来说, 95%的类都是我们自己写的, 因此, 而我们自己写的类是有应用程序类加载器加载. 其实,应用程序类加载器只有在第一次的时候, 才会加载两次. 以后, 当再次使用到这个类的时候, 直接去问应用程序类加载器, 有这个类么? 已经有了, 就直接返回了.


1.2 源码分析双亲委派机制


我们来看一下类加载器.类加载器主要调用的是classLoader.loadClass("com.lxl.Math") 这个方法来实现双亲委派机制的. 根据上面的分析, 我们知道, 在Launcher类初始化的时候, loadClass是AppClassLoader, 那么也就是说, 双亲委派机制的起点是AppClassLoader.


下面我们来看一下源码, 我们采用断点的方式来分析


首先, 我们在Launcher的AppClassLoader的loadClass(String var1, boolean var2) 这个方法添加一个断点, 并将其赋值为我们的com.lxl.jvm.Math类


然后运行Math的main方法,我们来看一下这个类到底是如何被加载的


启动debug调试模式, 首先进入了Launch.AppClassLoader.loadClass(....)方法


我们来具体看看这个方法的实现


上面都是在做权限校验, 我们看重点代码. 重点代码是调用了super.loadClass(var1,var2), 而这个super是谁呢? 我们来看看AppClassLoader的集成关系


在mac上按option+command+u查看集成关系图


我们看到AppClassLoader继承自URLClassLoader, 而URLClassLoader又继承了上面四个类,最终有继承一个叫做ClassLoader的类, 所有的类加载器, 最终都要继承这个ClassLoader类.


而这里调用的是super.loadClass(),我们来看看URLClassLoader中是否有loadClass()类, 看过之后发现,他没有, 最终这个super.loadClass()是继承了ClassLoader类的loadClass(....)方法


正是这个类实现了双亲委派机制, 下面我们就来看看, 他到底是怎么实现的?


当前的类加载器是AppClassLoader类加载器, 首先第一步是查找AppClassLoader中已经加载的类中,有没有这个类,


通过调用findLoadedClass(name)方法来查询已经加载的类中, 有没有com.lxl.jvm.Math类. 那么findLoadedClass(name)里面做了什么呢? 我们进去看看


我们看到, findLoaderClass(name)方法调用了自己的一个方法findLoadedClass0,//代码效果参考:http://hnjlyzjd.com/xl/wz_24289.html

这个方法是native的, 也就是是本地方法, 使用c++实现的, 我们不能看到底部的具体实现细节了. 但是大致的逻辑就是在已经加载的类中查找有没有com.lxl.jvm.Math这个类, 如果有就返回Class类信息.

debug看到,显然是没有的, 接下来就是走到if(c == null)里面了, 这里做了什么事呢?


他判断了,当前这个类加载器的parent是否是null. 我们知道当前这个类加载是AppClassLoader, 他的parent是ExtClassLoader, 自然不是null, 所以, 就会执行里面的parent.loadClass(name, false);


也就是执行扩展类加载器的loadClass(...)方法. 我们来看看扩展类ExtClassLoader


我们发现ExtClassLoader类里面没有loadClass(...)方法, 那他没有, 肯定就是在父类里定义的了, 通过查找, 最后我们发现这个方法还是ClassLoader里的loadClass(...)方法. 于是,我们继续debug.肯定会再次走到loadClass(...)这个方法里来. 而此时, loadClass是ExtClassloader的loadClass(...)方法


果然, 又走到这个方法里面来了


继续往下执行, 首先查找ExtClassLoader中已经加载的类中,是否有java.lxl.jvm.Math类, 过程和上面是一样的. 最后调用的是本地方法.


我们知道, 这肯定是没有的了. 然后继续判断, ExtClassLoader的parent是否为空. 很显然, 他就是空啊, 因为ExtClassLoader的父类加载器是引导类加载器BootStrapClassLoader, 而引导类加载器是c++写的,所以,这里的parent为空. parent为空执行的是else中的代码


这个方法就是去引导类加载器BootstrapClassLoad中查找, 是否有这个类, 我们来看看引导类加载器里面的具体实现


我们发现, 最后具体的逻辑也是一个本地方法实现的. 我们还是猜测一下, 这就是去查找引导类加载器已经加载的类中有没有com.lxl.jvm.Math, 如果有就返回这个类, 如果没有就返回null.


很显然, 是没有的. c == null. 我们继续来看下面的代码


到此为止, 我们第一次向上查找的过程就完完事了. 用图表示就是这样


首先有应用程序类加载器加载类, 判断应用程序已加载的类中, 是否有这个类, 结果是没有, 没有则调用其父类加载器ExtClassLoader的loadClass()方法, 去扩展类加载器中查找是否有这个类, 也没有. 那么判断其父类是否为空, 确实为空, 则进入到引导类加载器中取查找是否有这个类, 最后引导类加载器中也没有, 返回null


下面来看看类加载器是如何向下委派的?


引导类加载器中也没有这个类, 返回null, 接下来调用findClass(name);查找ExtClassLoader中是否有com.lxl.jvm.Math, 我们来看看具体的实现. 首先这是谁的方法呢?是ExtClassLoader的.


进入到findClass(name)方法中, 首先看看ExtClassLoader类中是否有这个方法, 没有, 这里调用的是父类UrlClassLoader中的findClass()方法


在findClass()里面, 我们看到将路径中的.替换为/,并在后面增加了.class. 这是在干什么呢? 不就是将com.lxl.jvm.Math替换为com/lxl/jvm/Math.class么


然后去resource库中查找是否有这个路径. 没有就返回null, 有就进入到defineClass()方法.


我们想一想, 在ExtClassLoader类路径里面能找到这个类么?显然是找不到的, 因为这个类使我们自己定义的.


他们他一定执行return null.


正如我们分析, debug到了return null; 这是执行的ExtClassLoader的findClass(). 返回null, 回到AppClassLoader加载类里面


c就是null, 然后继续执行findClass(name), 这是还是进入到了URLClassPath类的findClass(name)


如上图, 此时调用的是AppClassLoader的findClass(name), 此时的resource还是空么?当然不是了, 在target目录中就有Math.class类, 找到了, 接下来执行defineClass(name,res)


defindClass这个方法是干什么的呢? 这个方法就是加载类. 类已经找到了, 接下来要做的就是将其加载进来了.


这里执行的就是我们之前说的类加载的几个步骤,如下图红线圈出的部分.


好了,具体的就不说了. 后面有时间在继续分析.


这就是双亲委派机制的源码.


那么当下一次在遇到com.lxl.jvm.Math类的时候, 我们在AppClassLoader中就已经有了, 直接就返回了.


在来看一遍双亲委派机制的流程图


1.3 为什么要有双亲委派机制?


两个原因:


1. 沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改


2. 避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.


我们来看下面的案例


加入, 我在本地定义了一个String类, 包名是java.lang.String. 也就是是rt.jar包下的String类的包名是一样的哈.


如上图, 这是我们运行main方法, 会怎么样? 没错, 会报错


下面分析一下, 为什么会报错呢?


还是看双亲委派机制的流程, 首先由AppClassLoader类加载器加载, 看看已经加载的类中有没有java.lang.String这个类, 我们发现, 没有, 找ExtClassLoader加载, 也没有, 然后交给引导类BootStrapClassLoader加载, 结果能不能找到呢? 当然可以了. 但是这个java.lang.String是rt.jar中的类, 不是我们自定义的类, 加载了rt.jar中的java.lang.String类以后, 去找main 方法, 没找到.....结果就跑出了找不到main方法异常.


所以说, 如果我们自己定义的时候, 想要重新定义一个系统加载的类, 比如String.class, 可能么? 不可能, 因为自己定义的类根本不会被加载


这就是双亲委派机制的第一个作用: 沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改


双亲委派机制还有一个好处: 避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.

相关文章
|
Java 开发者 容器
【面试题精讲】JVM-打破双亲委派机制-OSGI模块化
【面试题精讲】JVM-打破双亲委派机制-OSGI模块化
|
7月前
|
Java 关系型数据库 MySQL
【JVM】JDBC案例打破双亲委派机制
【JVM】JDBC案例打破双亲委派机制
182 4
|
5月前
|
关系型数据库 MySQL Java
spi机制打破双亲委派机制
在JDBC4及以上版本,连接MySQL数据库不再需要显式加载驱动(`Class.forName`),而是利用SPI机制。系统通过扫描`META-INF/services/java.sql.Driver`文件找到`com.mysql.cj.jdbc.Driver`并使用`ServiceLoader`由AppClassLoader加载。`DriverManager`在启动时加载所有可用的`Driver`实现,实现解耦和动态发现。虽然看起来逆向了双亲委派,但实际上每个类仍由适当的类加载器加载,保持了加载层次。
spi机制打破双亲委派机制
|
7月前
|
Java
【JVM】双亲委派机制、打破双亲委派机制
【JVM】双亲委派机制、打破双亲委派机制
64 1
|
7月前
|
监控 前端开发 安全
JVM工作原理与实战(十一):双亲委派机制
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了双亲委派机制、父类加载器、双亲委派机制的主要作用、双亲委派机制常见问题等内容。
337 1
|
7月前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
408 2
|
7月前
|
监控 安全 前端开发
JVM工作原理与实战(十二):打破双亲委派机制-自定义类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、自定义类加载器等内容。
94 1
|
7月前
|
安全 前端开发 Java
大厂面试题详解:聊一下双亲委派机制
字节跳动大厂面试题详解:聊一下双亲委派机制
85 0
|
7月前
|
安全 前端开发 Java
【JVM】双亲委派机制详细解读(通俗易懂)
【JVM】双亲委派机制详细解读(通俗易懂)
655 0
|
Arthas 前端开发 安全
【面试题精讲】JVM-双亲委派机制
【面试题精讲】JVM-双亲委派机制

热门文章

最新文章