JVM-初识java虚拟机(四)---- 线程上下文类加载器

本文深入探讨了Java SPI(Service Provider Interface)机制的工作原理及其与线程上下文类加载器(TCCL)的关系。SPI允许系统在运行时动态加载和实例化模块的实现,而TCCL则打破了传统的类加载器双亲委派模型,使得程序能够逆向使用类加载器,从而解决SPI接口与其实现类加载路径不匹配的问题。

线程上下文类加载器(ThreadContextClassLoader)

引言:

SPI机制简介

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

SPI具体约定

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

SPI的使用(线程上下文类加载器出现的原因)

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有
JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到
SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

思考:

线程上下文类加载器如何打破了双亲委派模型?又是如何逆向使用类加载器了?
注:下文使用TCCL来表示线程上下文类加载器

JDBC案例分析——从源码角度进行深刻理解

我们先来看平时是如何使用mysql获取数据库连接的:

// 加载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"); 

以上就是mysql注册驱动及获取connection的过程,我们可以发现经常写的Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。、

思考:到底是在哪一步自动注册了mysql driver的呢?

让我们进入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() {
                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 下文件中的类名,使用TCCL加载;
通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。

我们来看刚才的代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
    } catch(Throwable t) {
// Do nothing
}

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取)。
:TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

再看下看ServiceLoader.load(Class)的代码,的确如此:

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

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

好,刚才说的驱动实现类就是com.mysql.jdbc.Driver.Class,它的静态代码块里头又写了什么呢?是否又用到了TCCL呢?

真正理解线程上下文类加载器的作用

com.mysql.jdbc.Driver加载后运行的静态代码块:

static {
 try {
  // Driver已经加载到TCCL中了,此时可以直接实例化
  java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
 } catch (SQLException E) {
  throw new RuntimeException("Can't register driver!");
 }
}

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。到此驱动注册基本完成。

接下来我们我们来看重要的代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }
     
     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }
     SQLException reason = null;
     // 遍历注册到registeredDrivers里的Driver类
     for(DriverInfo aDriver : registeredDrivers) {
         // 检查Driver类有效性
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 // 调用com.mysql.jdbc.Driver.connect方法获取连接
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }
             
         } else {
             println("    skipping: " + aDriver.getClass().getName());
         }
     }
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }

由于TCCL本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,isDriverAllowed方法就是校验存放的driver是否属于调用者的Classloader。

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
     // 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
 // 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
 // driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }
    return result;
}

基础篇-0-Java虚拟机导学课程 11:33 基础篇-1-初识JVM 22:27 基础篇-2-Java虚拟机的组成 04:47 基础篇-3-字节码文件的组成-以正确的姿势打开字节码文件 10:41 基础篇(补)-3.5-字节码文件的组成-基础信息 15:54 基础篇-4-字节码文件的组成-常量池和方法 25:51 基础篇-5-字节码文件常见工具的使用1 11:43 基础篇-6-字节码文件常见工具的使用2 22:20 基础篇-7-类的生命周期加载阶段 22:09 基础篇-8-类的生命周期2连接阶段 19:58 基础篇-9-类的生命周期3初始化阶段 26:27 基础篇-10-类加载器的分类 13:56 基础篇-11-启动类加载器 13:36 基础篇-12-扩展和应用程序类加载器 16:26 基础篇-13-双亲委派机制 18:43 基础篇-14-打破类的双亲委派机制-自定义类加载器 25:16 基础篇-15-打破双亲委派机制2-线程上下文类加载器 20:17 基础篇-16-打破双亲委派机制3-osgi和类的热部署 11:53 基础篇-17-JDK9之后的类加载器 09:05 基础篇-18-运行时数据区-程序计数器 15:42 基础篇-19--局部变量表 19:20 基础篇-20--操作数栈和帧数据 12:08 基础篇-21--内存溢出 15:28 基础篇-22-堆内存 25:56 基础篇-23-方法区的实现 16:25 基础篇-24-方法区-字符串常量池 20:40 基础篇-25-直接内存 12:39 基础篇-26-自动垃圾回收 11:32 基础篇-27-方法区的回收 11:32 基础篇-28-引用计数法 15:41 基础篇-29-可达性分析法 20:25 基础篇-30-软引用 24:40 基础篇-31-弱虚终结器引用 12:08 基础篇-32-垃圾回收算法的评价标准 13:31 基础篇-33-垃圾回收算法1 10:05 基础篇-34-垃圾回收算法-分代GC 20:19 基础篇-35-垃圾回收器1 15:54 基础篇-36-垃圾回收器2 11:44 基础篇-37-垃圾回收器3 15:51 基础篇-38-g1垃圾回收器 26:23 实战篇-1-内存泄漏和内存溢出 21:25 实战篇-2-解决内存泄漏-监控-top命令 12:16 实战篇-3-解决内存泄漏-监控-visualvm 12:50 实战篇-4-解决内存泄漏-监控-arthas tunnel 15:18 实战篇-5-解决内存泄漏-监控-prometheus-grafana 17:53 实战篇-6-解决内存泄漏-堆内存状况对比 08:39 实战篇-7-解决内存泄漏-内存泄漏产生的几大原因 16:01 实战篇-8-内存泄漏产生的原因2 13:30 实战篇-9-内存泄漏产生的原因3 10:43 实战篇-10-内存泄漏产生的原因4 10:04 实战篇-11-内存泄漏产生原因2-并发请求问题 17:30 实战篇-12-导出堆内存快照并使用MAT分析 08:38 实战篇-13-MAT内存泄漏检测原理 17:23 实战篇-14-服务器导出内存快照和MAT使用小技巧 13:31 实战篇-15-实战1-查询大数据量导致的内存溢出 26:24 实战篇-16-实战2-mybatis导致的内存溢出 10:34 实战篇-17-实战3-k8s容器环境导出大文件内存溢出 26:13 实战篇-18-系统不处理业务时也占用大量的内存 14:13 实战篇-19-文章审核接口的内存问题 18:28 实战篇-20-btrace和arthas在线定位问题 20:15 实战篇-21-GC调优的核心目标 11:23 实战篇-22-GC调优的常用工具 12:05 实战篇-23-GC调优的常见工具2 14:25 实战篇-24-常见的GC模式 13:38 实战篇-25-基础JVM参数的设置 28:31 实战篇-26-垃圾回收器的选择 18:04 实战篇-27-垃圾回收参数调优 07:56 实战篇-28-实战-GC调优和内存调优 30:43 实战篇-29-性能问题的现象和解决思路 10:49 实战篇-30-定位进程CPU占用率高的问题 18:52 实战篇-31-接口响应时间很长问题的定位 14:44 实战篇-32-火焰图定位接口响应时间长的问题 12:03 实战篇-33-死锁问题的检测 14:37 实战篇-34-基准测试框架JMH的使用 28:24 实战篇-35-实战-性能调优 26:36 高级篇-01-GraalVM介绍 12:13 高级篇-02-GraalVM的两种运行模式 15:43 高级篇-03-使用SpringBoot3构建GraalVM应用 15:08 高级篇-04-将GraalVM应用部署到函数计算 25:13 高级篇-05-将GraalVM应用部署到Serverless 09:14 高级篇-06-参数优化和故障诊断 22:31 高级篇-07-垃圾回收器的技术演进 13:09 高级篇-08-ShenandoahGC 22:50 高级篇-09-ZGC 14:35 高级篇-10-实战案例-内存不足时的垃圾回收测试 09:47 高级篇-11-JavaAgent技术 12:16 高级篇-12-JavaAgent环境搭建 15:24 高级篇-13-查看内存的使用情况 18:48 高级篇-14-生成内存快照 13:47 高级篇-15-获取类加载器的信息 16:26 高级篇-16-打印类的源码 18:00 高级篇-17-使用ASM增强方法 29:45 高级篇-18-使用ByteBuddy打印方法执行的参数和耗时 21:55 高级篇-19-APM系统和数据采集 24:30 原理篇-01-栈上的数据存储 15:05 原理篇-02-boolean在栈上的存储方式 22:48 原理篇-03-对象在堆上的存储1 17:27 原理篇-04-对象在堆上的存储2 25:14 原理篇-05-方法调用的原理1-静态绑定 19:26 原理篇-06-方法调用的原理2-动态绑定 15:25 原理篇-07-异常捕获的原理 12:00 原理篇-08-JIT即时编译器 14:49 原理篇-09-JIT即时编译器优化手段1-方法内联 16:49 原理篇-10-JIT即时编译器优化手段2-逃逸分析 09:03 原理篇-11-g1垃圾回收器原理-年轻代回收 27:57 原理篇-12-g1垃圾回收器原理-混合回收 17:24 原理篇-13-ZGC原理 26:27 原理篇-14-ShenandoahGC原理 09:39 面试篇-01-什么是JVM 16:38 面试篇-02-字节码文件的组成 15:02 面试篇-03-什么是运行时数据区 20:09 面试篇-04-哪些区域会出现内存溢出 11:56 面试篇-05-JDK6-8内存区域上的不同 14:36 面试篇-06-类的生命周期 17:17 面试篇-07-什么是类加载器 17:05 面试篇-08-什么是双亲委派机制 12:15 面试篇-09-如何打破双亲委派机制 18:10 面试篇-10-tomcat的自定义类加载器 31:18 面试篇-11-如何判断堆上的对象有没有被引用 10:05 面试篇-12-JVM中都有哪些引用类型 16:58 面试篇-13-theadlocal中为什么要使用弱引用 12:16 面试篇-14-有哪些垃圾回收算法 24:54 面试篇-15-有哪些常用的垃圾回收器 18:55 面试篇-16-如何解决内存泄漏问题 23:52 面试篇-17-常见的JVM参数 11:11 这是目前我学习的视频集合,要不要全看,或者少了什么,有哪些重要内容需要进行学习汇总或刷题或通过小实例验证
最新发布
10-02
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值