亲测双亲委派机制的缺陷及打破双亲委派机制

2024-04-20 14:58

本文主要是介绍亲测双亲委派机制的缺陷及打破双亲委派机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

双亲委派机制时JVM类加载的默认使用的机制,其原理是:当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。按照由父级到子集的顺序,类加载器主要包含以下几个:

  • BootstrapClassLoader(启动类加载器):主要负责加载核心的类库(java.lang.*等),JVM_HOME/lib目录下的,构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader (拓展类加载器):主要负责加载jre/lib/ext目录下的一些扩展的jar
  • AppletClassLoader(系统类加载器):主要负责加载应用程序的主函数类
  • 自定义类加载器:主要负责加载应用程序的主函数类

了解类加载器的基本原理和基本概念之后,进入我们今天的主题:

  1. 双亲委派机制有什么缺陷?
  2. 如何打破双亲委派机制?

问题1:通过双亲委派机制的原理可以得出一下结论:由于BootstrapClassloader是顶级类加载器,BootstrapClassloader无法委派AppClassLoader来加载类,也就是说BootstrapClassloader中加载的类中无法使用由AppClassLoader加载的类。可能绝大部分情况这个不算是问题,因为BootstrapClassloader加载的都是基础类,供AppClassLoader加载的类调用的类。但是万事万物都不是绝对的比如经典的JAVA SPI机制。

首先我们先了解下JAVA SPI机制:

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种 服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定:
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

以mysql-conneator-java-5.1.37Java包说明SPI机制:

以上截图展示了SPI使用的三要素:

  1. 实现类的java包位置要放在主程序的classpath中;
  2. 在实现类的jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  3. 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

这就引申出来我们对双亲委派机制的缺陷的讨论,接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,很明显java.sql包是由BootstrapClassloader加载器加载的;而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,我们的问题是DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖,那它是怎么解决这个问题的?这就引申了我们第二个问题:如何打破双亲委派机制?

首先看下手动获取数据库连接的代码:

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

我们很惊喜的发现:加载JDBC驱动程序实现的代码Class.forName("com.mysql.jdbc.Driver").newInstance();被注释掉,代码依然能够正常运行,这很奇怪, 继续查看DriverManager.getConnection(url,"name","password");重点就是DriverManager类的静态代码块,我们都是知道调用类的静态方法会初始化该类,然后执行该类静态代码块,DriverManager的静态代码块如下:

static {loadInitialDrivers();println("JDBC DriverManager initialized");}

继续查看 loadInitialDrivers();如下:

private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {//获取环境变量中jdbc.drivers的列表return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// If the driver is packaged as a Service Provider, load it.// Get all the drivers through the classloader// exposed as a java.sql.Driver.class service.// ServiceLoader.load() replaces the sun.misc.Providers()//如果按照spi的约定在jar包中的META-INF/services设置了文件,将会加载为服务AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();/* Load these drivers, so that they can be instantiated.* It may be the case that the driver class may not be there* i.e. there may be a packaged driver with the service class* as implementation of java.sql.Driver but the actual class* may be missing. In that case a java.util.ServiceConfigurationError* will be thrown at runtime by the VM trying to locate* and load the service.** Adding a try catch block to catch those runtime errors* if driver not available in classpath but it's* packaged as service and that service is there in classpath.*/try{while(driversIterator.hasNext()) {//依次加载所有驱动driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);       //如果环境变量中没有设置的驱动程序,就可以结束了//否则就将环境变量中的驱动程序加载一下if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}}
------------------------------------------------------------------------------------public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}

对于以上源码有几点说明:

  1. 我们前文提过JAVA SPI使用的扫描服务实现类的工具类是ServiceLoader,很凑巧我们在源码中发现了这个方法,这说明DriverManager.getConnection()方法在被调用的时候就已经从classpath中去加载由第三方实现的java.sql.Driver接口的实现类了。继续查看ServiceLoader.load(Driver.class);方法发现类加载器使用的是线程上下文类加载器,这是打破双亲委托机制的关键。
  2. 按照loadedDrivers.iterator()->next()->nextService()调用连查看源码最终发现c = Class.forName(cn, false, loader);这个方法是文章开头的Class.forName()被注释掉但是文章仍然能够继续运行的关键。因为在DriverManager中的初始化代码中已经注册过了。但在这里我有一个疑问1:既然驱动类已经已经在ServiceLoader.load(Driver.class)方法中被加载过了,为什么在Class.forName(cn, false, loader);方法中注册驱动类的时候还要传递一个类加载器的参数,这样做由什么意义?但是我们可以大胆的推测loader一定不是启动类加载器,因为启动类加载器没法加载classpath下的类。
  3. loadInitialDrivers()加载里两个位置的驱动程序(代码中已有注释),环境变量中jdbc.drivers的列表和类路径下符合SPI规范的jar包,前者使用的是Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());进行加载,而类加载器使用的是:ClassLoader.getSystemClassLoader();后者使用的是load(Driver.class)方法中的线程上下文类加载器。接下来我们看下ClassLoader.getSystemClassLoader();源码,按照ClassLoader.getSystemClassLoader()->initSystemClassLoader();>scl= l.getClassLoader()发现ClassLoader.getSystemClassLoader()的返回值是类Launcher的一个成员变量并且在Launcher的构造方法中进行初始化,最终的返回值是this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);应用类加载器(查看下方截图1处代码),在这里我有一个疑问2:Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());这段代码所在类的类加载器是启动类加载器,但是代码中使用了应用类加载器,这样可以使用吗?如果可以那在启动类加载器加载的类中使用应用类的时候直接指定应用类加载器去加载就可以了,为什么还要使用线程上下文类加载器?

以上两个疑问后续解决,不影响我们对如何打破双亲委托机制的讨论,现在我们已经知道,在DriverManager中去加载SPI中配置的java.sql.Driver接口的实现类使用的是线程上下文类加载器。ContextClassLoader默认存放了AppClassLoader的引用(查看下方截图2处代码),由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成加载类的操作,简单来说:在BootstrapClassLoader或ExtClassLoader加载的类A中如果使用到AppClassLoader类加载器加载的类B,由于双亲委托机制不能向下委托,那可以在类A中通过线上线程上下文类加载器获得AppClassLoader,从而去加载类B,这不是委托,说白了这是作弊,也是JVM为了解决双亲委托机制的缺陷不得已的操作!

拓展:

简单介绍一下Class.forName();这个方法由两个作用:

  1. 装载一个类并对其进行实例化
  2. Class.forName();使用的类加载器默认是当前类加载器,但是可以为之传递一个加载器

源码如下:

public static Class<?> forName(String className)throws ClassNotFoundException {Class<?> caller = Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);}
------------------------------------------------------------------------------------------
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)throws ClassNotFoundException{Class<?> caller = null;SecurityManager sm = System.getSecurityManager();if (sm != null) {// Reflective call to get caller class is only needed if a security manager// is present.  Avoid the overhead of making this call otherwise.caller = Reflection.getCallerClass();if (sun.misc.VM.isSystemDomainLoader(loader)) {ClassLoader ccl = ClassLoader.getClassLoader(caller);if (!sun.misc.VM.isSystemDomainLoader(ccl)) {sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);}}}return forName0(name, initialize, loader, caller);}

这篇关于亲测双亲委派机制的缺陷及打破双亲委派机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/920565

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

【Tools】大模型中的自注意力机制

摇来摇去摇碎点点的金黄 伸手牵来一片梦的霞光 南方的小巷推开多情的门窗 年轻和我们歌唱 摇来摇去摇着温柔的阳光 轻轻托起一件梦的衣裳 古老的都市每天都改变模样                      🎵 方芳《摇太阳》 自注意力机制(Self-Attention)是一种在Transformer等大模型中经常使用的注意力机制。该机制通过对输入序列中的每个元素计算与其他元素之间的相似性,

如何通俗理解注意力机制?

1、注意力机制(Attention Mechanism)是机器学习和深度学习中一种模拟人类注意力的方法,用于提高模型在处理大量信息时的效率和效果。通俗地理解,它就像是在一堆信息中找到最重要的部分,把注意力集中在这些关键点上,从而更好地完成任务。以下是几个简单的比喻来帮助理解注意力机制: 2、寻找重点:想象一下,你在阅读一篇文章的时候,有些段落特别重要,你会特别注意这些段落,反复阅读,而对其他部分

【Tools】大模型中的注意力机制

摇来摇去摇碎点点的金黄 伸手牵来一片梦的霞光 南方的小巷推开多情的门窗 年轻和我们歌唱 摇来摇去摇着温柔的阳光 轻轻托起一件梦的衣裳 古老的都市每天都改变模样                      🎵 方芳《摇太阳》 在大模型中,注意力机制是一种重要的技术,它被广泛应用于自然语言处理领域,特别是在机器翻译和语言模型中。 注意力机制的基本思想是通过计算输入序列中各个位置的权重,以确

FreeRTOS内部机制学习03(事件组内部机制)

文章目录 事件组使用的场景事件组的核心以及Set事件API做的事情事件组的特殊之处事件组为什么不关闭中断xEventGroupSetBitsFromISR内部是怎么做的? 事件组使用的场景 学校组织秋游,组长在等待: 张三:我到了 李四:我到了 王五:我到了 组长说:好,大家都到齐了,出发! 秋游回来第二天就要提交一篇心得报告,组长在焦急等待:张三、李四、王五谁先写好就交谁的

10个好用的AI写作工具【亲测免费】

1. 光速写作 传送入口:http://u3v.cn/6hXWYa AI打工神器,一键生成文章&ppt 2. 讯飞写作 传送入口:http://m6z.cn/5ODiSw 3. 讯飞绘文 传送入口:https://turbodesk.xfyun.cn/?channelid=gj3 4. AI排版助手 传送入口:http://m6z.cn/6ppnPn 5. Kim

UVM:callback机制的意义和用法

1. 作用         Callback机制在UVM验证平台,最大用处就是为了提高验证平台的可重用性。在不创建复杂的OOP层次结构前提下,针对组件中的某些行为,在其之前后之后,内置一些函数,增加或者修改UVM组件的操作,增加新的功能,从而实现一个环境多个用例。此外还可以通过Callback机制构建异常的测试用例。 2. 使用步骤         (1)在UVM组件中内嵌callback函