第一次接触线程上下文类加载器是在看《深入JVM虚拟机》的时候,当时一直只是停留在理论层面去理解线程上下文类加载器(TCCL,ThreadContextClassLoader),这回结合了其他人的理解,算是大彻大悟了。
记笔记记笔记.jpg

什么是TCCL?

简单来说, TCCL 仅仅是 Thread 类的一个属性:

  1. public class Thread implements Runnable {
  2. /* The context ClassLoader for this thread */
  3. private ClassLoader contextClassLoader;
  4. public ClassLoader getContextClassLoader() {
  5. if (contextClassLoader == null)
  6. return null;
  7. SecurityManager sm = System.getSecurityManager();
  8. if (sm != null) {
  9. ClassLoader.checkClassLoaderPermission(contextClassLoader,
  10. Reflection.getCallerClass());
  11. }
  12. return contextClassLoader;
  13. }
  14. public void setContextClassLoader(ClassLoader cl) {
  15. SecurityManager sm = System.getSecurityManager();
  16. if (sm != null) {
  17. sm.checkPermission(new RuntimePermission("setContextClassLoader"));
  18. }
  19. contextClassLoader = cl;
  20. }
  21. }

通过 Thread 对象保存 ClassLoader ,达到了只需要通过 Thread 对象就能获取到想要的任意一种 ClassLoader 的目的。
虽然看起来感觉使用 TCCL 简单,但是这背后牵涉的原理作用却蛮复杂的。

为什么需要TCCL?

首先我们需要知道几个前置知识点:

  1. 每一个类加载器负责的加载路径不同,上层的类加载器无法访问下层类加载器加载的资源,但是下层的类加载器可以访问上层类加载器加载的资源(双亲委派模型)
  2. 一个 Class 是否相同,需要考虑加载它们的类加载器是否相同。
  3. 如果一个类是被 ClassLoader A 加载的,那么通过这个类加载的所有类都是属于 ClassLoader A 的。注意,如果某个 Class 已经被加载进来,只是单纯的 new 一下,那么这个 Class 属于加载它的 ClassLoader

现在要提出正常的场景: B.class 需要 new A() ,因为 B.classAppClassLoader 加载的,所以使用 new 时默认使用 AppClassLoader 加载器。 AppClassLoader 因为遵循双亲委派模型,所以会先去询问 BootClassLoaderBootClassLoader 找到 A.Class 之后就返回给 AppClassLoaderAppClassLoader 就能 new 这个 A.class 了 :
点击查看【processon】
假设此时 A.class 需要加载另外一个由 AppClassLoader 负责加载的资源 C.class 时,因为双亲委派模型+每个加载器只能加载自己负责的空间,所以 BootClassLoader 无法查找到 AppClassLoader 负责的 C.class
点击查看【processon】
那么为什么需要 TCCL
就是为了解决 上层类加载器 不能使用 下层类加载器 加载不属于它的资源空间
我们看下图所示:
点击查看【processon】
我们遇到的核心问题是—— BootClassLoader 无法获取到下层类加器 AppClassLoader 。因此 sun 需要一个东西来中转一下下层的 ClassLoader ,那么最终是挑选了一个 Thread 作为这个中转站。因为 ThreadBootClassLoader 加载的,每一个线程独有一个,可以通过 Thread.currentThread() 获取到当前的线程,不需要额外的代码,粒度刚刚好;至于为什么不是 Object ,我想这是因为粒度太细了, Object 对象太多不好管理;

案例

JDBC+SPI

以往(JDK1.6以前)要使用 JDBC 时,需要这么写:

  1. // 从AppClassLoader加载驱动,然后在驱动里面触发static,将自身注册到DriverManager
  2. Class.forName("com.mysql.jdbc.Driver");
  3. String url = "jdbc:mysql://localhost:3306/testdb";
  4. // 通过java库获取数据库连接
  5. 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类不能加载,需要线程上下文加载器。能解释一下他们的区别吗?

这位朋友没有弄清楚 BootClassLoaderAppClassLoader 负责的类路径空间。 SPI 类是来自于 BootClassLoader ,通过 BootClassLoader 加载不到 AppClassLoader ,所以需要线程上下文加载器。