jvm(1)类的加载(三)(线程上下文加载器)

我怕爱的太早我们不能终老 提交于 2020-11-26 08:03:32

 简介:

类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。
Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。
现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。

1,java.lang.ClassLoader类介绍

1,java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。
2,除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

主要方法:

getParent()                     返回该类加载器的父类加载器。
loadClass(String name)          加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name)          查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name)    查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len)    把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c)        链接指定的 Java 类。

ps:

loadClass方法包括findClass和findLoadedClass的步骤。findClass包括findLoadedClass的步骤;
findLoadedClass(是本地方法)从内存中查找指定的类;
findClass主要是查找字节码文件。

2,类加载器的树状组织结构

public class ClassLoaderTree {  
   public static void main(String[] args) { 
       ClassLoader loader = ClassLoaderTree.class.getClassLoader(); 
       while (loader != null) { 
           System.out.println(loader.toString()); 
           loader = loader.getParent(); 
       }   } }
运行结果
sun.misc.Launcher$AppClassLoader@9304b1 
sun.misc.Launcher$ExtClassLoader@190d11

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用。

通过递归调用 getParent()方法来输出全部的父类加载器。

没有输出引导类加载器,因为对于父类加载器是引导类加载器,getParent()方法返回 null

3,类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。

特别注意一点:Java 虚拟机是如何判定两个 Java 类是相同的。

Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。

比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class

两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。

这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException

例子如下:com.example.Sample 类

package com.example; 
 
public class Sample { 
   private Sample instance; 
 
   public void setSample(Object instance) { 
       this.instance = (Sample) instance; 
   } 
}

com.example.Sample类的方法 setSample接受一个 java.lang.Object类型的参数,并且会把该参数强制转换成 com.example.Sample类型。

测试 Java 类是否相同
public void testClassIdentity() { 
   String classDataRootPath = "C:\\workspace\\Classloader\\classData"; 
   FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); 
   FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); 
   String className = "com.example.Sample";    
   try { 
       Class<?> class1 = fscl1.loadClass(className); 
       Object obj1 = class1.newInstance(); 
       Class<?> class2 = fscl2.loadClass(className); 
       Object obj2 = class2.newInstance(); 
       Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); 
       setSampleMethod.invoke(obj1, obj2); 
   } catch (Exception e) { 
       e.printStackTrace(); 
   } 
}

使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的 java.lang.Class的实例,

接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample

试图把对象 obj2赋值给 obj1内部的 instance对象。

测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException 
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) 
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597) 
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26) 
at classloader.ClassIdentity.main(ClassIdentity.java:9) 
Caused by: java.lang.ClassCastException: com.example.Sample 
cannot be cast to com.example.Sample 
at com.example.Sample.setSample(Sample.java:7) 
... 6 more

运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。

虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。

了解了这一点之后,就可以理解代理模式的设计动机了:

代理模式是为了保证 Java 核心库的类型安全。
所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。
如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。
通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。 不同的类加载器为相同名称的类创建了额外的名称空间。
相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,
这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

PS:以上是虚拟机层面的比较,也可以通过方法比较,如:equeals()等。

4,加载类的过程

类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。

真正完成类的加载工作是通过调用 defineClass来实现的(称为一个类的定义加载器(defining loader));
而启动类的加载过程是通过调用 loadClass来实现的(称为初始加载器(initiating loader))。
在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。

两种类加载器的关联之处在于:

一个类的定义加载器是它引用的其它类的初始加载器。
如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

异常:

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;
方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。

 


PS:以上都是加载层面需要明确加载器,从应用层面来说更多只是对字节码对象的使用。

 

5,线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。

类 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。

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

Java 应用运行的初始线程的上下文类加载器系统类加载器

在线程中运行的代码可以通过此类加载器来加载类和资源

为什么需要使用线程上下文加载器:

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。

常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。 这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。
SPI 接口中的代码经常需要加载具体的实现类。
如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。
这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。
如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。

而问题在于:

SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的

SPI 实现的 Java 类一般是由系统类加载器来加载的。

引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。
它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。

也就是说,类加载器的代理模式无法解决这个问题。

 

线程上下文类加载器正好解决了这个问题。

如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。
在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。

PS:有一个疑问,线程上下文加载器加载时候实际也是应用加载器为什么说这样就可以了。

以上来源:深入探讨 Java 类加载器

 

 

java类加载器不完整分析

真正理解线程上下文类加载器(多案例分析)

 深入理解Java类加载器(2):线程上下文类加载器

深入理解Java类加载器(一):Java类加载原理解析

深入理解Java类加载器(二):线程上下文类加载器

注册即获取引用字节码的引用。

JVM高级特性与实践(九):类加载器 与 双亲委派模式(自定义类加载器源码探究ClassLoader)

JVM类加载器原理与自定义类加载器

PS:为什么要使用线程上下文类加载。

关键在于:
哪个加载器加载的类去调用其它加载器加载的类。

首先理解双亲加载模式存在的问题。

默认情况应用执行,从main()所在的类开始,是采用系统类加载器,到扩展类加载器,再到启动类加载,进行查找和加载类。
类调用其引用的类是在当前类加载器内,或者其父加载器中类(也就是库中类)。
类的使用关联类,隐含了类加载器查找类的过程
也就是说调用类的过程是和类加载器层次是一致的,只能从下往上。上层加载器加载的类,却是无法使用下次加载器加载的类的。 这种模式也就是双亲加载模式。

一个错误理解:

认为类都是被这三个加载器加载。然后加载到同一个地方。
从源码中看到类查找也都是使用父类ClassLoader的findClassLoader()。
认为他们三者加载的类是共享,平等的,他们之间是可以随便调用的。
其实不然,类在底层是通过类加载器隔离的。
之所有上面那种错觉,

是因为平常我们都是从下层往上层使用的。

一般情况下,一个类和所关联的类都是通过同一个类加载器加载的,这样方便查找,顶多还去类加载器的父加载器中查找。

但是有些情况,有些类事先已经被父加载器加载了。而他的实际实现类,却被子加载器或者其他加载器加载了。

在调用这些类的功能时候如何查找使用那些实际实现类?

那么只需要把实际实现类的类加载器设置成线程上下文类加载器就可以了。
在使用类的功能,就可以通过线程上下文类加载去加载或者查找那些实际实现类。

为什么使用线程上下文加载器,而不是直接获得加载器。

1,直接获取需要的类加载器,那么类加器就会被固定了。
2,如果不想固定,那么只能通过参数层层传递。
因为使用线程加载器要灵活。 比如,一个加载器加载那些实际实现类,启动类加载加载类,要调用这些类,首先需要得到那个类加载,从而才能找到实际实现类。
那么存在层层传递这个类加载器给启动类加载器加载的类。 而使用线程上下文就简单很多,可以在这个线程共享这个类加载器。

一般来说上下文类加载器要比当前类加载器适合于框架编程,而当前类加载器则更适合于业务逻辑编程。

可怕的线程上下文类装载器(TCCL)

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!