【Java虚拟机11】线程上下文类加载器

2023-02-14,,,,

前言

目前学习到的类加载的知识,都是基于【双亲委托机制】的。那么JDK难道就没有提供一种打破双亲委托机制的类加载机制吗?

答案是否定的。

JDK为我们提供了一种打破双亲委托模型的机制:线程上下文类加载器。

1.引出【线程上下文类加载器】

我们来复习一下JDBC执行SQL的代码【画外音:感觉回到了大学自学Java的时候】:

Connection conn = DriverManager.getConnection(url, user, password);
String sql = "insert into user (name, pwd) values(?,?)";
Statement st = conn.createStatement();
st.executeQuery(sql);

重点关注一下接口Connection,Statement接口,大家都很清楚,这2个接口是JDK提供给各个数据库厂商的通过JAVA访问数据库的接口。

而这些接口是在包java.sql里面,而这个包是在rt.jar这个包中。所以这些JDK所提供的标准接口的Class文件是被启动类加载器所加载的。

而数据库厂商:如MySQL/Oracle根据Java语言的标准所提供的驱动包,如:mysql-connector-java-5.1.38.jar,这些驱动包一般在项目中都仅仅是在classpath目录下,

并不能被启动类加载器加载,只能被系统类加载器加载。

那么问题来了:Connection conn = DriverManager.getConnection(url, user, password);

方法返回的实例其实是一个com.mysql.jdbc.ConnectionImpl(应该被AppClassLoader加载),

而方法返回值的定义为java.sql.Connection(应该被BootstrapClassLoader加载)。

根据前面学习的命名空间的逻辑:“由父类加载器加载的类不能看见子加载器加载的类”,按这一规则来说,获取Connection的代码是不能执行成功的。

其实JDK已经想到并帮我们处理了这个问题。

它使用了一种叫做线程上下文类加载器的CLassLoader机制,来打破双亲委托机制在某些场景无法满足扩展的需求,实现这种场景的应用。

2.概念

2.1当前类加载器(Current Class Loader)

每个类都会使用自己的类加载器(即加载自身的类加载器)去加载其他类(指的是所依赖的类),

如果ClassX引用了ClassY,那么ClassX的类加载器就会先去加载ClassY(前提是ClassY尚未被加载过)。

2.2线程上下文类加载器(Thread Context Class Loader)

线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设值上下文类加载器。

如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。

JAVA应用运行时的初始线程的上下文类加载器是系统类加载器(AppClassLoader)。在线程中运行的代码可以通过该类加载器来加载类与资源。

线程上下文类加载器就是当前线程的Current Class Loader

2.3线程上下文类加载器是如何打破双亲委托模型的

SPI (Service Provider Interface)

父classloader可以使用当前线程Thread.currentThread().getContextLoader()所指定的classloader加载的类。

这就改变了父classloader不能使用子classloader或是其他没有直接父子关系的classloader加载类的情况,即改变了双亲委托模型。

在双亲委托模型下,类加载是由下至上的,即下层的加载器会委托上层进行加载,但是对于SPI来说,有些接口是JAVA核心库所提供的。

而JAVA核心库是由启动类加载器来加载的,而这些接口的实现是来自于不同的jar包(厂商的提供),JAVA的启动类加载器是不会加载其他来源的jar包。

这样传统的双亲委托模型就无法满足SPI的要求。

而通过给当前线程设置上下文类加载器就可以由设置的上下文类加载器来实现对接口实现类的加载。(打破双亲委托模型限制)

3.默认的线程上下文类加载器

3.1 Launcher实例化时,默认设置线程上下文类加载器

在上次的文章【Java虚拟机10】ClassLoader.getSystemClassLoader()流程简析第二步中,简要介绍了Launcher实例的初始化过程,其中有一句代码当时是一笔带过的,这次拿出来再看看。

就是在Launcher类构造方法,首先初始化了ExtClassLoader,然后初始化了AppClassLoader,之后调用了

    public Launcher() {
//balabala.....
Thread.currentThread().setContextClassLoader(this.loader);
//balabala.....
}

3.2 ClassLoader.getSystemClassLoader()时,默认设置线程上下文类加载器

依然在上一篇文章【Java虚拟机10】ClassLoader.getSystemClassLoader()流程简析第四步中,也存在一个设置默认的线程上下文类加载器的代码。

可以看出,虚拟机默认的线程上下文类加载器为系统类加载器(AppClassLoader)

4.线程上下文类加载器的一般使用模式

线程上下文类加载器的一般使用模式为:

ClassLoader originClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(myClassLoader);
myMethod();
//myMethod里面则调用了Thread.currentThread().getContextClassLoader(),用外面设置的加载器(myClassLoader)去完成一些自己希望完成的事情。
} finally {
Thread.currentThread().setContextClassLoader(originClassLoader);
}

5.总结

如果一个类由类加载器A加载,那么这个类的依赖类也会被类加载器A加载(前提是这个依赖类尚未被加载过)。
ThreadContextClassLoader的存在就是为了破坏双亲委托模型。
当高层提供了统一的接口让低层去实现,同时又要在高层去加载(或实例化)低层的类的时候,就必须用ThreadContextClassLoader来帮助高层的ClassLoader来找到并加载低层类。

【Java虚拟机11】线程上下文类加载器的相关教程结束。

《【Java虚拟机11】线程上下文类加载器.doc》

下载本文的Word格式文档,以方便收藏与打印。