JVM 线程上下文类加载器

3,427 阅读5分钟

前言

在类加载器的命名空间中说过

  • 子加载器所加载的类能够访问父加载器所加载的类。
  • 父加载器所加载的类无法访问到子加载器所加载的类。

线程上下文类加载器(Context Classloader)

当前类加载器(Current classloader)

  • 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类)。
  • 如果classX引用了classY,那么ClassX的类加载器就会去加载classY(前提是classY尚未被加载)。

线程上下文类加载器(Context Classloader)

  • 线程上下文类加载器是从JDK 1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoade)分别用来获取和设置上下文类加载器
  • 如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。
  • Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。
  • 要非常明确,该加载器是动态的,在运行期间才有的类加载器。
  • 在运行期间动态的加载需要的类,是可以跨越不同的类加载器边界的。

线程上下文类加载器的重要性

  • SPI ( Service Provider Interface)
  • 父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的Classloader加载的类。这就改变了父ClasSLoader不能使用子ClassLoader或是其他没有直接父子关系的classLoader加载的类的情况,即改变了双亲委托模型
  • 线程上下文类加载器就是当前线程的当前类加载器(Current Classloader)。
  • 在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供), Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

关于线程上下文类加载器的说明

  • 首先什么是上下文(Context),就是语境,类似高中的语文的阅读理解题,根据上下文说明主人公为什么不吃饭?这样的感觉。在这就是代码的内容。
    • 上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
  • 它就是用来弥补授权委托机制的不足点的。
    • 上面说了,SPI其实就像JBDC那种。其实Java内置有关JBDC的接口,由不同公司自己去实现那个接口。
    • MySQL的驱动jar包大家都要额外入导吧。 image.png

    就是这个Driver由不同厂商实现,MySql和Oracle各不相同。

  • 按照双亲委托的这个思路:
    • 首先在使用JDBC时,rt.jar中的接口会被BootStrapClassLoader加载,然后它的实现类由AppClassLoader加载,因为导入的驱动是在classPath路径下的。
    • 然后,在使用Driver时,因为父加载器所加载的类无法访问到子加载器所加载的类。也就是说在用Driver时,它就是个空壳子,找不到它的实现类。
    • 面向接口编程,就像下面那样,接口指向子类,怕是行不通了:
    Runnable runnable = new RunableImpl();
    Driver driver = new DriverImpl();
    
  • 为了防止出现上述情况,设置线程上下文类加载让加载实现类和接口的类加载是一样就可以解决了。 -在运行时,根据线程上下文类加载去加载SPI,来准确的加载接口的实现类。

tomcat中就大量使用上下文类加载器。里面涉及到不同war之间类加载器的隔离,以及加载不同目录下的class等。

代码示例

当前类加载器

  • 代码
public class ContextTest {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(Thread.class.getClassLoader());
    }
}
  • 结果

image.png

线程上下文类加载器

  • 默认情况下,线程上下文类加载器就是AppClassLoader
    public class ContextTest implements Runnable {
      private Thread thread;
    
      public ContextTest() {
          this.thread = new Thread(this);
          this.thread.start();
      }
      @Override
      public void run() {
          ClassLoader classLoader = this.thread.getContextClassLoader();//返回此线程的上下文类加载器
          //this.thread.setContextClassLoader(classLoader);
          System.out.println("Class :"+classLoader.getClass());
          System.out.println("Parent :"+classLoader.getParent().getClass());
      }
    
      public static void main(String[] args) {
          new ContextTest();
      }
    }
    
  • 结果 image.png
  • 为什么默认是AppClassLoader
    • 阅读Launcher类jdk源码 image.png
    • 在加载类时就已经set好了上下文类加载器。
    • 以AppClassLoader为默认上下文类加载是因为可以根据它可以找到它的父类加载器。即可以找到所有的jdk自带的类加载器。

上下文类加载器的使用模式

  • 线程上下文类加载器的一般使用模式(获取–使用–还原)
  classLoader classLoader = Thread.currentThread().getContextClassLoader();
  try {
      Thread.currentThread ().setContextClassLoader(targetTccl) ;
      myMethod();
  }finally {
      Thread.currentThread ().setContextClassLoader (classLoader) ;
  }
  • myMethod里面则调用了Thread.currentThread( ).getContextClassLoader(),获取当前线程的上下文类加载器做某些事情。如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话)。
  • ContextClassLoader的作用就是为了破坏Java的类加载委托机制。
  • 当高层提供了统一的接口(不一定是interface,可以是规定某些功能细节的东西)让低层去实现,同时又要在高层加载〈或实例化))低层的类时,就必须要通过线程上下文类加载器来帮助高层的classLoa找到并加载该类。
  • 其实就不用双亲委托了,就直接用指定的类加载器加载类,能加载的就都加载了。