JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器

2 阅读6分钟

前言

JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。


一、打破双亲委派机制的方法

双亲委派机制的核心思想是:当一个类加载器接收到加载类的请求时,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader)中去,只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过再由顶向下进行加载。

双亲委派机制是Java类加载器的重要特性,但在某些情况下,可能需要打破这种机制。以下是打破双亲委派机制的方法:

  • 自定义类加载器:在Java中,可以通过继承ClassLoader并重写其loadClass方法来创建自定义类加载器。通过这种方式,可以打破双亲委派机制,实现类的隔离。例如,在Tomcat中,每个Web应用都有自己的类加载器,从而实现了应用之间的类隔离。当两个Web应用中有相同限定名的类时,如Servlet类,Tomcat通过自定义类加载器保证它们是不同的类。
  • 线程上下文类加载器:在Java中,每个线程都有一个关联的上下文类加载器。通过设置线程的上下文类加载器,可以实现类的加载。例如,JDBC和JNDI等就是利用线程上下文类加载器来加载类的。
  • Osgi框架的类加载器:Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载。在Osgi中,每个Bundle都有自己的类加载器,当需要加载类时,会先在自己的存储空间中查找,如果找不到,则委托给父级Bundle的类加载器进行查找。这种机制打破了传统的双亲委派模型。

二、线程上下文类加载器

双亲委派机制是Java类加载器的一个重要特性,它保证了Java核心API的稳定性和防止了类冲突。然而,在某些情况下,可能需要打破这种机制。接下来将重点探讨如何通过线程上下文类加载器来实现这一点,并以JDBC中的驱动加载为例进行深入解析。

线程上下文类加载器(Thread Context ClassLoader,TCCL)是Java中一种特殊的类加载器,用于处理多线程环境下的类和资源加载问题。它确保了每个线程都能维护自己的类加载器上下文,从而实现了线程安全。线程上下文类加载器是解决线程局部变量和类加载问题的有效机制。当线程需要加载类时,它会首先使用与其关联的上下文类加载器,从而确保了类加载的一致性和正确性。

1.JDBC线程上下文类加载器案例

在JDBC中,DriverManager用于管理不同数据库的驱动。例如,MySQL的驱动和Oracle的驱动。DriverManager类位于rt.jar包中,由启动类加载器加载。然而,用户JAR包中的驱动需要由应用程序类加载器来加载,这就违反了双亲委派机制。

为了解决这个问题,JDBC采用了SPI(Service Provider Interface)机制。SPI是一种JDK内置的服务提供发现机制。

在ClassPath路径下的META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件中写该接口的实现:

例如,在MySQL JDBC驱动中,有一个名为java.sql.Driver的文件在META-INF/services文件夹中,文件内容为com.mysql.cj.jdbc.Driver。这样,当DriverManager需要加载MySQL驱动时,它会首先查看当前线程的上下文类加载器(通常是应用程序类加载器)是否能加载这个驱动。由于驱动的实现类是由应用程序类加载器加载的,这就打破了双亲委派机制。

使用ServiceLoader加载实现类:

使用ServiceLoader加载对应的Driver对象:

ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);

在这个过程中,DriverManager使用了SPI机制来加载jar包中对应的驱动类。SPI机制通过在META-INF/services目录下查找实现了特定接口的类的配置文件,并使用线程上下文类加载器来加载这些实现类。

SPI机制中使用的线程上下文类加载器通常是应用程序类加载器。这是因为线程上下文类加载器是线程特有的,每个线程都可以关联一个不同的上下文类加载器。这使得SPI机制能够灵活地加载类,打破了双亲委派机制的限制。

在ServiceLoader的load方法中,通过获取当前线程的上下文类加载器来实现类的加载。

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

为了验证线程上下文类加载器的使用,可以创建一个新的线程并在其中打印线程上下文类加载器的信息。

public class NewThreadDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getContextClassLoader());
            }
        }).start();
    }
}

运行结果(应用程序类加载器):

完整流程分析

  1. 启动类加载器加载DriverManager。
  2. 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
  3. SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。


总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容,希望对大家有所帮助。