java八股文面试[JVM]——如何打破双亲委派模型

  1. 双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类,典型的打破双亲委派模型的框架和中间件tomcatosgi

  2. 双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()。即线程上下文类加载器(contextClassLoader)。双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?线程上下文类加载器就出现了。

    1. SPI。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

    2. 线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题

  3. 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用

前言
比较两个类是否“相等”,前提是这两个类由同一个类加载器加载,
否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,
只要加载它们的类加载器不同,那么这两个类就必定不相等。
打破双亲委派

 如下是一个自定义的类加载器TestClassLoader,并重写了findClass和loadClass:

public class TestClassLoader extends ClassLoader {public TestClassLoader(ClassLoader parent) {super(parent);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 1、获取class文件二进制字节数组byte[] data = null;try {System.out.println(name);String namePath = name.replaceAll("\\.", "\\\\");String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";ByteArrayOutputStream baos = new ByteArrayOutputStream();FileInputStream fis = new FileInputStream(new File(classFile));byte[] bytes = new byte[1024];int len = 0;while ((len = fis.read(bytes)) != -1) {baos.write(bytes, 0, len);}data = baos.toByteArray();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}// 2、字节码加载到 JVM 的方法区,// 并在 JVM 的堆区建立一个java.lang.Class对象的实例// 用来封装 Java 类相关的数据和方法return this.defineClass(name, data, 0, data.length);}@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException{Class<?> clazz = null;// 直接自己加载clazz = this.findClass(name);if (clazz != null) {return clazz;}// 自己加载不了,再调用父类loadClass,保持双亲委托模式return super.loadClass(name);}
}

测试:初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:

public static void main(String[] args) throws Exception {// 初始化TestClassLoader,被将加载TestClassLoader类的类加载器设置为TestClassLoader的parentTestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());// 加载 DemoClass clazz = testClassLoader.loadClass("study.stefan.classLoader.Demo");System.out.println("Demo的类加载器:" + clazz.getClassLoader());
}

运行如下测试代码,发现报错了:
找不到java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?

转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo,也会加载父类Object。Object和study.stefan.classLoader.Demo并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass如下:
遇到前缀为java.的就去找官方的class文件。

运行测试代码:
还是报错了!!! 报错信息为:Prohibited package name: java.lang

看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。

得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载

跳过AppClassLoaderExtClassLoader
既然如此,先将study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader:

package com.stefan.DailyTest.classLoader;public class Test {public static void main(String[] args) throws Exception {// 初始化TestClassLoader,并将加载TestClassLoader类的类加载器// 设置为TestClassLoader的parentTestClassLoader testClassLoader = new TestClassLoader(null);System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());// 加载 DemoClass clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");System.out.println("Demo的类加载器:" + clazz.getClassLoader());}
}

双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass。
运行测试代码:

成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。

如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派

一个比较完整的自定义类加载器

一般情况下,自定义类加载器都是继承URLClassLoader,具有如下类关系图:

tomcat是如何打破双亲委派的

Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoader,WebappClassLoader继承自URLClassLoader,重写了findClass和loadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader。
WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoader,ExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。

Web应用默认的类加载顺序是(打破了双亲委派规则):

先从JVM的BootStrapClassLoader中加载。
加载Web应用下/WEB-INF/classes中的类。
加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
加载上面定义的System路径下面的类。
加载上面定义的Common路径下面的类。


如果在配置文件中配置了``,那么就是遵循双亲委派规则,加载顺序如下:

先从JVM的BootStrapClassLoader中加载。
加载上面定义的System路径下面的类。
加载上面定义的Common路径下面的类。
加载Web应用下/WEB-INF/classes中的类。
加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。


1 Tomcat对用户类库与类加载器的规划
在其目录结构下有三组目录(“/common/”、“/server/”、“/shared/”)可以存放Java类库,另外还可以加上Web应用程序本身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:

放置在/commom目录中:类库可被Tomcat和所有的Web应用程序共同使用
放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见
放置在/shared目录中:类库可被所有的Web应用程序所共同使用,但对Tomcat自己不可见
放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,所下图:

最上面的三个类加载器是JDK默认提供的类加载器,这三个加载器的的作用之前也说过,这里不再赘述了,而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader则是Tomcat自己定义的类加载器,他们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和jsp类加载器通常会存在多个实例每一个Web应用程序对应一个WebApp类加载器,每一个jsp文件对应一个Jsp类加载器

从上图的委派关系可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的哪一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过在建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能

tomcat对于不同应用需要有不同的隔离环境
tomcat给每个应用都创建了一个WebApp ClassLoader类加载器
重写了load方法:不再向上查找,而是在本类查找不到后再向上。对于其他的需要共享的例如Redis,可以在上层Share ClassLoader中共享。

OSGI是如何打破双亲委派的

既然说到OSGI,就要来解释一下OSGi是什么,以及它的作用

OSGi(Open Service Gateway Initiative):是OSGi联盟指定的一个基于Java语言的动态模块化规范,这个规范最初是由Sun、IBM、爱立信等公司联合发起,目的是使服务提供商通过住宅网管为各种家用智能设备提供各种服务,后来这个规范在Java的其他技术领域也有不错的发展,现在已经成为Java世界中的“事实上”的模块化标准,并且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是Eclipse IDE

OSGi中的每一个模块(称为Bundle)与普通的Java类库区别并不大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明他允许导出发布的Java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上如此),而且类库的可见性能得到精确的控制,一个模块里只有被Export过的Package才可能由外界访问,其他的Package和Class将会隐藏起来。除了更精确的模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑性的特性

OSGi之所以能有上述“诱人”的特点,要归功于它灵活的类加载器架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundle声明了一个它依赖的Package,如果有其他的Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会为派给发布他的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器是平级关系,只有具体使用某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖

另外,一个Bundle类加载器为其他Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在于Bundle的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGi平台也不会把其他Bundle的类加载请求分配给这个Bundle来处理

一个例子:假设存在BundleA、BundleB、BundleC三个模块,并且这三个Bundle定义的依赖关系如下:

BundleA:声明发布了packageA,依赖了java.*的包
BundleB:声明依赖了packageA和packageC,同时也依赖了Java.*的包
BundleC:声明发布了packageC,依赖了packageA
那么,这三个Bundle之间的类加载器及父类加载器之间的关系如下图:

由于没有涉及到具体的OSGi实现,所以上图中的类加载器没有指明具体的加载器实现,只是一个体现了加载器之间关系的概念模型,并且只是体现了OSGi中最简单的加载器委派关系。一般来说,在OSGi中,加载一个类可能发生的查找行为委派关系会比上图中显示的复杂,类加载时的查找规则如下:

以java.*开头的类,委派给父类加载器加载
否则,委派列表名单内的类,委派给父类加载器加载
否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载
否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment bundle的类加载器加载
否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
否则,查找失败
从之前的图可以看出,在OSGi里面,加载器的关系不再是双亲委派模型的树形架构,而是已经进一步发展成了一种更复杂的、运行时才能确定的网状结构

相关面试题:一个类的静态块是否可能被执行两次

一个自于网易面试官的一个问题,一个类的静态块是否可能被执行两次。

答案:如果一个类,被两个 osgi的bundle加载, 然后又有实例被初始化,其静态块会被执行两次

什么是SPI 机制

Spi 机制加载第三方扩展的jar包类初始化。
mysql, dubbo rpc


SPi机制的原理:
java SPI全称Service Provider Interface 。是java 提供的一套用来被第三方实现的API,他可以用来启用框架扩展和替换组件。实际上是基于接口编程+策略模式+配置文件 组合实现的动态加载机制

JDBC

原本的JDBC: Class.forName(“DriverName”) 是通过调用Driver中静态代码块中的将Driver注册

public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}
}

使用SPI的JDBC:
在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver,然后可以直接调用

Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

问题是 :一个类的加载器和调用他的加载器相同
这里调用的是 bootstrap类加载器,无法加载到子类厂商中的类


方法:使用线程上下文加载器

public class DriverManager {static {loadInitialDrivers();println("JDBC DriverManager initialized");}private static void loadInitialDrivers() {//省略代码//这里就是查找各个sql厂商在自己的jar包中通过spi注册的驱动ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}//省略代码}
}

使用Thread类的 getContextClassLoader

    public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){return new ServiceLoader<>(service, loader);}

整个mysql的驱动加载过程:

第一,获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
第二,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第三,通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端

 SPI参考:39 如何破坏双亲委派机制原则 - 简书

知识来源:

JVM问题(一) -- 如何打破双亲委派模型_如何打破双亲委派机制_leo_messi94的博客-CSDN博客

打破双亲委派的几种办法_破坏双亲委派_hhpub的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/98496.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习-4-二维目标检测-YOLOv5理论模型详解

YOLOv5理论模型详解 1.Yolov5四种网络模型 Yolov5官方代码中&#xff0c;给出的目标检测网络中一共有4个版本&#xff0c;分别是Yolov5s、Yolov5m、Yolov5l、Yolov5x四个模型。 YOLOv5系列的四个模型&#xff08;YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x&#xff09;在参数量和性…

【python基础知识】5.for循环和while循环

文章目录 前言for...in...循环语句for循环&#xff1a;空房间for循环&#xff1a;一群排队办业务的人range()函数for循环&#xff1a;办事流程 while循环while循环&#xff1a;放行条件while循环&#xff1a;办事流程 两种循环对比 前言 上一关&#xff0c;我们学习了两种新的…

Springboot 实践(13)spring boot 整合RabbitMq

前文讲解了RabbitMQ的下载和安装&#xff0c;此文讲解springboot整合RabbitMq实现消息的发送和消费。 1、创建web project项目&#xff0c;名称为“SpringbootAction-RabbitMQ” 2、修改pom.xml文件&#xff0c;添加amqp使用jar包 <!-- RabbitMQ --> <dependency&g…

Unity 中 Pivot 与 Center,重置模型物体的轴心为中心

文章目录 1. 概念2. 动态计算正确的模型中心点Center3. 重置模型物体的轴心为中心 1. 概念 在 Unity 面板的左上角有两个按钮&#xff0c;在本文中主要研究 Pivot/Center Pivot&#xff08;轴心&#xff09;&#xff1a;模型的真实位置&#xff0c;是由建模软件设定的可在模…

哈夫曼编码实现文件的压缩和解压

程序示例精选 哈夫曼编码实现文件的压缩和解压 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《哈夫曼编码实现文件的压缩和解压》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0…

签到系统怎么设计

背景 相信签到系统大家都有接触过&#xff0c;更多的是使用。但是有思考过这种系统是怎么设计的吗&#xff1f;比方说我统计一下每个月中每天的签到情况&#xff0c;怎么设计呢&#xff1f;今天一篇文章告诉你。 首先&#xff0c;我们熟悉的思维是&#xff1a;我设计一个数据…

MySQL主从复制和读写分离搭建

目录 一、主从复制原理 1、MySql支持从复制类型 2、主从复制的原理⭐⭐⭐ 4、mysql主从复制延迟 异步、同步、半同步复制&#xff1a; 二、主从复制实验 1、mysql 时间同步 1.1 主服务设置被同步的时间 1.2 两台从服务器设置时间同步&#xff08;两台服务器一样配置&am…

VSCode 配置 C 语言编程环境

目录 一、下载 mingw64 二、配置环境变量 三、三个配置文件 四、格式化代码 1、安装插件 2、保存时自动格式化 3、左 { 不换行 上了两年大学&#xff0c;都还没花心思去搭建 C 语言编程环境&#xff0c;惭愧&#xff0c;惭愧。 一、下载 mingw64 mingw64 是著名的 C/C…

练习2:88. 合并两个有序数组

这里写自定义目录标题 题目解体思路代码 题目 给你两个按非递减顺序排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m和 n &#xff0c;分别表示 nums1 和 nums2中的元素数目。 请你合并nums2 到 nums1 中&#xff0c;使合并后的数组同样按非递减顺序排列。 注意&a…

JVM类加载器

一、类与类加载器 类加载器虽然只用于实现类的加载动作&#xff0c;但它在Java程序中起到的作用却远超类加载阶段。对于 任意一个类&#xff0c;都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性&#xff0c;每一个类加载器&#xff0c;都拥有一个独…

骨传导耳机危害有哪些?骨传导耳机值得入手吗?

事实上&#xff0c;只要是正常使用&#xff0c;骨传导耳机并不会对身体造成伤害&#xff0c;并且在众多耳机种类中&#xff0c;骨传导耳机可以说是相对健康的一种耳机&#xff0c;这种耳机最独特的地方便是声波不经过外耳道和鼓膜&#xff0c; 而是直接将人体骨骼结构作为传声介…

Django(10)-项目实战-对发布会管理系统进行测试并获取测试覆盖率

在发布会签到系统中使用django开发了发布会签到系统, 本文对该系统进行测试。 django.test django.test是Django框架中的一个模块,提供了用于编写和运行测试的工具和类。 django.test模块包含了一些用于测试的类和函数,如: TestCase:这是一个基类,用于编写Django测试用…