第一次接触线程上下文类加载器是在看《深入JVM虚拟机》的时候,当时一直只是停留在理论层面去理解线程上下文类加载器(TCCL,ThreadContextClassLoader),这回结合了其他人的理解,算是大彻大悟了。
什么是TCCL?
简单来说, TCCL
仅仅是 Thread
类的一个属性:
public class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
}
通过 Thread
对象保存 ClassLoader
,达到了只需要通过 Thread
对象就能获取到想要的任意一种 ClassLoader
的目的。
虽然看起来感觉使用 TCCL
简单,但是这背后牵涉的原理作用却蛮复杂的。
为什么需要TCCL?
首先我们需要知道几个前置知识点:
- 每一个类加载器负责的加载路径不同,上层的类加载器无法访问下层类加载器加载的资源,但是下层的类加载器可以访问上层类加载器加载的资源(双亲委派模型)。
- 一个
Class
是否相同,需要考虑加载它们的类加载器是否相同。 - 如果一个类是被
ClassLoader A
加载的,那么通过这个类加载的所有类都是属于ClassLoader A
的。注意,如果某个Class
已经被加载进来,只是单纯的new
一下,那么这个Class
属于加载它的ClassLoader
现在要提出正常的场景: B.class
需要 new A()
,因为 B.class
是 AppClassLoader
加载的,所以使用 new
时默认使用 AppClassLoader
加载器。 AppClassLoader
因为遵循双亲委派模型,所以会先去询问 BootClassLoader
, BootClassLoader
找到 A.Class
之后就返回给 AppClassLoader
, AppClassLoader
就能 new
这个 A.class
了 :
点击查看【processon】
假设此时 A.class
需要加载另外一个由 AppClassLoader
负责加载的资源 C.class
时,因为双亲委派模型+每个加载器只能加载自己负责的空间,所以 BootClassLoader
无法查找到 AppClassLoader
负责的 C.class
:
点击查看【processon】
那么为什么需要 TCCL
?
就是为了解决 上层类加载器 不能使用 下层类加载器 加载不属于它的资源空间
。
我们看下图所示:
点击查看【processon】
我们遇到的核心问题是—— BootClassLoader
无法获取到下层类加器 AppClassLoader
。因此 sun
需要一个东西来中转一下下层的 ClassLoader
,那么最终是挑选了一个 Thread
作为这个中转站。因为 Thread
是 BootClassLoader
加载的,每一个线程独有一个,可以通过 Thread.currentThread()
获取到当前的线程,不需要额外的代码,粒度刚刚好;至于为什么不是 Object
,我想这是因为粒度太细了, Object
对象太多不好管理;
案例
JDBC+SPI
以往(JDK1.6以前)要使用 JDBC
时,需要这么写:
// 从AppClassLoader加载驱动,然后在驱动里面触发static,将自身注册到DriverManager
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
但自从引入 JDK1.6
后引入了 SPI
机制后(这个版本也引入了 TCCL
),现在就不再需要 Class.forName()
,因为在 DriverManager
中,会自动根据 /META-INF/services/java.sql.Driver
去查找具体底层实现并通过 TCCL
机制加载进来。
我们看一下现在的 DriverManager
是如何加载各个厂商的 Driver
吧~
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
private static void loadInitialDrivers() {
// 省略部分是通过System.getProperty来加载驱动的
...
// 通过SPI获取可加载的Driver
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 获取ServiceLoader,使用TCCL构建ServiceLoader
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 获取ServiceLoader的迭代器(此时还没有查询驱动)
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 加载并注册每一个驱动
try{
// 判断是否存在驱动
while(driversIterator.hasNext()) {
// 如果存在就获取
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
// 省略,和SPI无关
...
}
// loadedDrivers.iterator();进入
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 方法,判断是否有下一个元素,这里一直深入进去会去查询资源
public boolean hasNext() {
// 检查缓存的Driver是否存在
if (knownProviders.hasNext())
return true;
// lookupIterator 是后文的LazyIterator,相当于调用LazyIterator的hasNext()
return lookupIterator.hasNext();
}
// 方法,获取下一个元素,这里一直深入进去会去加载资源
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
private class LazyIterator implements Iterator<S>
{
// 去查询支持的Driver
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 如果配置为null
if (configs == null) {
try {
// PREFIX为META-INF/services,service.getName()要查询的路径
String fullName = PREFIX + service.getName();
// 如果loader为空,就是用BootClassLoader;一般这里都是TCCL
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 使用TCCL去加载要查询的资源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 如果查出来的路径为空
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析所有Driver资源为全限定类名
pending = parse(service, configs.nextElement());
}
// 保存所有资源中的下一个
nextName = pending.next();
return true;
}
// 如果启用了AccessController,调用hasNextServie();反之使用安全检测再执行
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
}
当 driversIterator.hasNext()
为 true
,就去加载并初始化 Driver
:
private class LazyIterator implements Iterator<S>
{
// 所有查询到的资源URL
Enumeration<URL> configs = null;
// 解析后的Driver全限定类名
Iterator<String> pending = null;
String nextName = null;
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加载Driver类,loader是TCCL
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 判断驱动实现是否实现了java.sql.Driver
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 初始化Driver,并强转为java.sql.Driver并返回
S p = service.cast(c.newInstance());
// 缓存这个Driver
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
}
网友提问
1:首先加载他的父类object。根据双亲委派模型。object会被委托给引导类加载器加载。 2:加载person类。 根据双亲委派模型会被系统类加载器加载。 然后加载完成。这个过程。假如用在spi里面。那么就是。 1:首先根据双亲委派模型 引导类加载器加载spi的接口。2系统类加载器加载spi的实现。 这个有什么问题吗??为什么说spi类不能加载,需要线程上下文加载器。能解释一下他们的区别吗?
这位朋友没有弄清楚 BootClassLoader
和 AppClassLoader
负责的类路径空间。 SPI
类是来自于 BootClassLoader
,通过 BootClassLoader
加载不到 AppClassLoader
,所以需要线程上下文加载器。