类加载器,双亲委派,破坏双亲委派机制的方法,SPI(服务提供者接口)
类加载器总结
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载
%JAVA_HOME%/lib
目录下的jar包和类或者或被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载目录
%JRE_HOME%/lib/ext
目录下的jar包和类,或被java.ext.dirs
系统变量所指定的路径下的jar包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
双亲委派模型的好处
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。
如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
加载类的逻辑,在ClassLoader的loadClass()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //首先,检查该类是否已经被加载过了 Class<?> c = findLoadedClass(name); //如果没有加载过,就调用父类加载器的loadClass()方法 if (c == null ) { long t0 = System.nanoTime(); try { if (parent != null ) { c = parent.loadClass(name, false ); } else { //如果父类加载器为空,就使用启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //如果在父类加载器中找不到该类,就会抛出ClassNotFoundException } if (c == null ) { //如果父类找不到,就调用findClass()来找到该类。 long t1 = System.nanoTime(); c = findClass(name); //记录统计数据 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } |
不提倡开发人员覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,
则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型。
SPI(服务提供者接口)
SPI的接口由Java核心库来提供,而这些SPI的实现代码则是作为Java应用所依赖的jar包被包含进类路径(ClassPath)里。接口由java核心库定义,但实现却有不同的jar包决定
SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的
线程上下文类加载器(Thread Context ClassLoader),在JVM中会把当前线程的类加载器加载不到的类交给线程上下文类加载器来加载,直接使用Thread.currentThread().getContextClassLoader()来获得,默认返回的就是应用程序类加载器,也可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。
而线程上下文类加载器破坏了双亲委派模型,也就是父类加载器请求子类加载器去完成类加载的动作,但为了实现功能,这也是一种巧妙的实现方式。
例子:JDBC
1、
1 2 3 4 5 6 | try { // Class.forName(driver); conn = (Connection)DriverManager.getConnection(url, user, passwd); } catch (Exception e) { System.out.println(e); } |
这种方式与第一种方式唯一的区别就是经常写的Class.forName被注释掉了,但程序依然可以正常运行,这是为什么呢?这是因为,从JDK1.6开始,Oracle就修改了加载JDBC驱动的方式,即JDBC4.0。在JDBC 4.0中,我们不必再显式使用Class.forName()方法明确加载JDBC驱动。当调用getConnection方法时,DriverManager会尝试自动设置合适的驱动程序。前提是,只要mysql的jar包在类路径中。
2、
重点就在DriverManager.getConnection()中。我们知道,调用类的静态方法会初始化该类,而执行其静态代码块是初始化类过程中必不可少的一环。DriverManager的静态代码块:
1 2 3 4 | static { loadInitialDrivers(); println( "JDBC DriverManager initialized" ); }<br> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | private static void loadInitialDrivers() { String drivers; try { // 先读取系统属性 : 对应上面第二种驱动注册方式 drivers = AccessController.doPrivileged( new PrivilegedAction<String>() { public String run() { return System.getProperty( "jdbc.drivers" ); } }); } catch (Exception ex) { drivers = null ; } // 通过SPI加载驱动类 AccessController.doPrivileged( new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver. class ); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { // Do nothing } return null ; } }); // 加载系统属性中的驱动类 : 对应上面第二种驱动注册方式 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); // 使用AppClassloader加载 Class.forName(aDriver, true , ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println( "DriverManager.Initialize: load failed: " + ex); } } } |
从上面可以看出,JDBC中的DriverManager加载Driver的步骤顺序依次是:
- 通过SPI方式,读取 META-INF/services 下文件中的类名,使用线程上下文类加载器加载;
- 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。
直白一点说就是:我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载。但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里,后续你想怎么操作就是你的事了。
总结:
通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:
-
当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
-
当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
- 参考:https://blog.csdn.net/justloveyou_/article/details/72231425
privatestaticvoidloadInitialDrivers(){ String drivers;try{// 先读取系统属性 : 对应上面第二种驱动注册方式 drivers = AccessController.doPrivileged(newPrivilegedAction<String>(){public String run(){return System.getProperty("jdbc.drivers");}});}catch(Exception ex){ drivers = null;}// 通过SPI加载驱动类 AccessController.doPrivileged(newPrivilegedAction<Void>(){public Void run(){ ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()){ driversIterator.next();}}catch(Throwable t){// Do nothing}return null;}});// 加载系统属性中的驱动类 : 对应上面第二种驱动注册方式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);// 使用AppClassloader加载 Class.forName(aDriver,true, ClassLoader.getSystemClassLoader());}catch(Exception ex){println("DriverManager.Initialize: load failed: "+ ex);}}}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)