Java的ThreadContext类加载器的知识点总结

  • 更新时间:2020-02-06 08:36:06
  • 编辑:任涵忍

疑惑

以前在看源码的时候,总是会遇到框架里的代码使用Thread.currentThread.getContextClassLoader()获取当前线程的Context类加载器,通过这个Context类加载器去加载类。

我们平时在程序中写代码的时候,遇到要动态加载类的时候,一般使用Class.forName()的方式加载我们需要的类。比如最常见的,当我们进行JDBC编程的时候,我们通过Class.forName()去加载JDBC的驱动。

try {
 return Class.forName("oracle.jdbc.driver.OracleDriver");
} catch (ClassNotFoundException e) {
 // skip
}

那么为什么当我们使用Class.forName()的方式去加载类的时候,如果类找不到,我们还要尝试用Thread.currentThread.getContextLoader()获取的类加载器去加载类呢?比如我们可能会碰到下面这种代码:

try {
 return Class.forName(className);
} catch (ClassNotFoundException e) {
 // skip
}

ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
if (ctxClassLoader != null) {
 try {
 clazz = ctxClassLoader.loadClass(className);
 } catch (ClassNotFoundException e) {
   // skip
 }
}

这里加粗的部分,就是使用了Thread.currentThread.getContextLoader()获取的加载器去加载类。显然,Class.forName()加载类的时候使用的类加载器可能和Thread.currentThread.getContextLoader()获取的类加载器是不同的。那么为什么会出现不同呢?

JAVA的类加载器

要理解为什么会用到Thread.currentThread.getContextLoader()获取的这个类加载器之前,我们先来了解下JVM里使用的类加载器(ClassLoader)。

JVM默认有三种类加载器:

  1. Bootstrap Class Loader
  2. Extension Class Loader
  3. System Class Loader

Bootstrap Class Loader

Bootstrap Class Loader类加载器是JDK自带的一款类加载器,用于加载JDK内部的类。Bootstrap类加载器用于加载JDK中$JAVA_HOME/jre/lib下面的那些类,比如rt.jar包里面的类。Bootstrap类加载器是JVM的一部分,一般采用native代码编写。

Extension Class Loader

  Extension Class Loader类加载器主要用于加载JDK扩展包里的类。一般$JAVA_HOME/lib/ext下面的包都是通过这个类加载器加载的,这个包下面的类基本上是以javax开头的。

System Class Loader

System Class Loader类加载器也叫应用程序类加载器(AppClassLoader)。顾名思义,这个类加载器就是用来加载开发人员自己平时写的应用代码的类的。System类加载器是用于加载存放在classpath路径下的那些应用程序级别的类的。

下面的代码列举出了这三个类加载器:

public class MainClass {
 public static void main(String[] args) {
  System.out.println(Integer.class.getClassLoader());
  System.out.println(Logging.class.getClassLoader());
  System.out.println(MainClass.class.getClassLoader());
 }
}

其中获取Bootstrap类加载器永远返回null值

null # Bootstrap类加载器
sun.misc.Launcher$ExtClassLoader@5e2de80c # Extension类加载器
sun.misc.Launcher$AppClassLoader@18b4aac2 # System类加载器

双亲委派模型

上面介绍的三种类加载器,并不是孤立的,他们之间有一个层次关系:

Java的ThreadContext类加载器的实现

三个类加载器之间通过这个层次关系协同工作,一起负责类的加载工作。上面的这种层次模型称为类加载器的“双亲委派”模型。双亲委派模型要求,除了最顶层的Bootstrap类加载器之外,所有的类加载器都必须有一个parent加载器。当类加载器加载类的时候,首先检查缓存中是否有已经被加载的类。如果没有,则优先委托它的父加载器去加载这个类,父加载器执行和前面子加载器一样的工作,直到请求达到顶层的Bootstrap类加载器。如果父加载器不能加载需要的类,那么这个时候才会让子加载器自己去尝试加载这个类。工作原理类似于下面这种方式:

  Java的ThreadContext类加载器的实现

我们可以通过JDK里ClassLoader里面的代码一窥双亲委派机制的实现方式,代码实现在ClassLoader.loadClass()里面

rotected Class<?> loadClass(String name, boolean resolve)
 throws ClassNotFoundException

 synchronized (getClassLoadingLock(name)) {
  // First, check if the class has already been loaded
  Class<?> c = findLoadedClass(name);
  if (c == null) {
   long t0 = System.nanoTime();
   try {
    if (parent != null) {
     c = parent.loadClass(name, false);
    } else {
     c = findBootstrapClassOrNull(name);
    }
   } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
   }

   if (c == null) {
    // If still not found, then invoke findClass in order
    // to find the class.
    long t1 = System.nanoTime();
    c = findClass(name);

    // this is the defining class loader; record the stats
    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    sun.misc.PerfCounter.getFindClasses().increment();
   }
  }
  if (resolve) {
   resolveClass(c);
  }
  return c;
 }

采用双亲委派的方式组织类加载器,一个好处是为了安全。如果我们自己定义了一个String类,希望将这个String类替换掉默认Java中的java.lang.String的实现。

我们将自己实现的String类的class文件放到classpath路径下,当我们使用类加载器去加载我们实现的String类的时候,首先,类加载器会将请求委托给父加载器,通过层层委派,最终由Bootstrap类加载器加载rt.jar包里的String类型,然后一路返回给我们。在这个过程中,我们的类加载器忽略掉了我们放在classpath中自定义的String类。

如果没有采用双亲委派机制,那么System类加载器可以在classpath路径中找到String的class文件并加载到程序中,导致JDK中的String实现被覆盖。所以类加载器的这种工作方式,在一定程度上保证了Java程序可以安全稳定的运行。

线程上下文类加载器

上面讲了那么多类加载器相关的内容,可还是没有讲到今天的主题,线程上下文类加载器。

到这里,我们已经知道Java提供了三种类加载器,并且按照严格的双亲委派机制协同工作。表面上,似乎很完美,但正是这种严格的双亲委派机制导致在加载类的时候,存在一些局限性。

当我们更加基础的框架需要用到应用层面的类的时候,只有当这个类是在我们当前框架使用的类加载器可以加载的情况下我们才能用到这些类。换句话说,我们不能使用当前类加载器的子加载器加载的类。这个限制就是双亲委派机制导致的,因为类加载请求的委派是单向的。

虽然这种情况不多,但是还是会有这种需求。比较典型的,JNDI服务。JNDI提供了查询资源的接口,但是具体实现由不同的厂商实现。这个时候,JNDI的代码是由JVM的Bootstrap类加载器加载,但是具体的实现是用户提供的JDK之外的代码,所以只能由System类加载器或者其他用户自定义的类加载器去加载,在双亲委派的机制下,JNDI获取不到JNDI的SPI的实现。

为了解决这个问题,引入了线程上下文类加载器。通过java.lang.Thread类的setContextClassLoader()设置当前线程的上下文类加载器(如果没有设置,默认会从父线程中继承,如果程序没有设置过,则默认是System类加载器)。有了线程上下文类加载器,应用程序就可以通过java.lang.Thread.setContextClassLoader()将应用程序使用的类加载器传递给使用更顶层类加载器的代码。比如上面的JNDI服务,就可以利用这种方式获取到可以加载SPI实现的类加载器,获取需要的SPI实现类。

Java的ThreadContext类加载器的实现

可以看到,引入线程类加载器实际是对双亲委派机制的破坏,但是却提供了类加载的灵活性。

解惑

回到开头,框架的代码为了加载框架之外用户实现的类,由于这些类可能没法通过框架使用的类加载器进行加载,为了绕过类加载器的双亲委派模型,采用Thread.getContextClassLoader()的方式去加载这些类。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持码农之家。

相关教程

  • 2020年初学者学Java看什么书好

    最近有许多的同学们准备或已开始学习Java,然而不少人对于学习Java看什么书很是困惑。事实却是如此,那今天小编就给大家推荐一些适合初学者学Java的书籍,希望可以帮助到大家:

    发布时间:2021-06-06

  • 作为程序员必须掌握的Java虚拟机中的22个重难点(推荐0

    这篇文章主要介绍了Java虚拟机中22个重难点,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

    发布时间:2019-07-24

  • Java微服务实战

    Java微服务实战

    大小:43.2MBJava实战电子书

  • Head First Java(中文)

    Head First Java(中文)

    本书是一本完整地面向对象(object-oriented,OO)程序设计和Java的学习指导用书,图文并茂学习方式能让你快速地在脑海中掌握住知识,欢迎下载使用

    大小:48.8 MBJava学习电子书

  • 云原生Java

    云原生Java

    不论是传统式IT制造行业,還是it行业,都正处在制造行业在历史上*强烈的转型中:很多的系统软件已经从传统式的IT构架刹车应用场景云的构架,开发方式也已经从开发设计和运维管理职责分

    大小:272.4 MB系统设计电子书

  • 你不知道的JavaScript(中卷)

    你不知道的JavaScript(中卷)

    JavaScript这门语言简单易用,很容易上手,但其语言机制复杂微妙,即使是经验丰富的JavaScript开发人员,如果没有认真学习的话也无法真正理解。

    大小:7.8 MBJS电子书

  • Java面向对象编程(第2版)

    Java面向对象编程(第2版)

    本书以行云流水般的语言诠释Java编程艺术,基于全新Java技术、理论紧密结合实际,详细讲解Java面向对象的编程思想、编程语法和设计模式等,可作为开发指南、高校教材和认证辅导教材,

    大小:136 MBJava编程电子书

  • Java EE企业级应用开发教程

    Java EE企业级应用开发教程

    这本书详细讲解了Java EE中Spring、Spring MVC和MyBatis三大框架,采用了大量案例,帮助读者学习和理解SSM的核心技术,是一本J2EE程序设计的轻量级开发教程

    大小:127.1 MBJava EE电子书

  • JavaScript高级程序设计

    JavaScript高级程序设计

    《 JavaScript高級编程设计(第3版) 》是JavaScript超級畅销书的*版。ECMAScript5和HTML5在规范之战中同时获胜,使很多特有保持和手机客户端拓展宣布进到标准,另外也为JavaScript增加了许多融入发展方

    大小:23 MBJS程序设计电子书

  • Java Web开发实例大全:提高卷

    Java Web开发实例大全:提高卷

    本书筛选、汇集了Java Web开发从基础知识到高级应用各个层面,含600经典实例及源码分析,23个应用方向,两卷共1200例,46个方向,供学习、速查、实践练习的超全参考手册

    大小:179.4 MBJava Web电子书

用户留言