jvm(1)类的加载(三)(线程上下文加载器)
简介:
类加载器从 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 类加载器
注册即获取引用字节码的引用。
JVM高级特性与实践(九):类加载器 与 双亲委派模式(自定义类加载器源码探究ClassLoader)
PS:为什么要使用线程上下文类加载。
关键在于:
哪个加载器加载的类去调用其它加载器加载的类。
首先理解双亲加载模式存在的问题。
默认情况应用执行,从main()所在的类开始,是采用系统类加载器,到扩展类加载器,再到启动类加载,进行查找和加载类。
类调用其引用的类是在当前类加载器内,或者其父加载器中类(也就是库中类)。
(类的使用关联类,隐含了类加载器查找类的过程)
也就是说调用类的过程是和类加载器层次是一致的,只能从下往上。上层加载器加载的类,却是无法使用下次加载器加载的类的。
这种模式也就是双亲加载模式。
一个错误理解:
认为类都是被这三个加载器加载。然后加载到同一个地方。
从源码中看到类查找也都是使用父类ClassLoader的findClassLoader()。
认为他们三者加载的类是共享,平等的,他们之间是可以随便调用的。
其实不然,类在底层是通过类加载器隔离的。
之所有上面那种错觉,
是因为平常我们都是从下层往上层使用的。
一般情况下,一个类和所关联的类都是通过同一个类加载器加载的,这样方便查找,顶多还去类加载器的父加载器中查找。
但是有些情况,有些类事先已经被父加载器加载了。而他的实际实现类,却被子加载器或者其他加载器加载了。
在调用这些类的功能时候如何查找使用那些实际实现类?
那么只需要把实际实现类的类加载器设置成线程上下文类加载器就可以了。
在使用类的功能,就可以通过线程上下文类加载去加载或者查找那些实际实现类。
为什么使用线程上下文加载器,而不是直接获得加载器。
1,直接获取需要的类加载器,那么类加器就会被固定了。
2,如果不想固定,那么只能通过参数层层传递。
因为使用线程加载器要灵活。
比如,一个加载器加载那些实际实现类,启动类加载加载类,要调用这些类,首先需要得到那个类加载,从而才能找到实际实现类。
那么存在层层传递这个类加载器给启动类加载器加载的类。
而使用线程上下文就简单很多,可以在这个线程共享这个类加载器。
一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。
jvm(1)类的加载(三)(线程上下文加载器)的更多相关文章
- JVM 线程上下文类加载器
当前类加载器(Current ClassLoader) 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指所依赖的类) 如果ClassX引用了ClassY,那么ClassX的类加载 ...
- jvm(1)类加载(一)(加载过程,双亲加载)
JVM类加载器机制与类加载过程 jvm虚拟机的种类: Hotspot(Oracle)(基本上都是在说这个) J9, JikesRVM(IBM) Zulu, Zing (Azul) Launcher是一 ...
- 【JVM学习笔记】线程上下文类加载器
有许多地方能够看到线程上下文类加载的设置,比如在sun.misc.Launcher类的构造方法中,能够看到如下代码 先写一个例子建立感性认识 public class Test { public st ...
- 深入理解Java类加载器(二):线程上下文类加载器
摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...
- jvm(1)类的加载(二)(自定义类加载器)
[深入Java虚拟机]之四:类加载机制 1,从Java虚拟机的角度,只存在两种不同的类加载器: 1,启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有其他 ...
- JVM:java类的加载机制
原文连接:https://www.cnblogs.com/ityouknow/p/5603287.html 类加载机制的奥妙. 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读 ...
- java中三个类别加载器的关系以及各自加载的类的范围
Java在需要使用类别的时候,才会将类别加载,Java的类别载入是由类别载入器(Class loader)来达到的,预设上,在程序启动之后,主要会有三个类别加载器:Bootstrap Loader.E ...
- <JVM上篇:内存与垃圾回收篇>02-类加载子系统
笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...
- 学习笔记TF049:TensorFlow 模型存储加载、队列线程、加载数据、自定义操作
生成检查点文件(chekpoint file),扩展名.ckpt,tf.train.Saver对象调用Saver.save()生成.包含权重和其他程序定义变量,不包含图结构.另一程序使用,需要重新创建 ...
随机推荐
- 测试用例Excel模板For Quality Center
Subject Test Name Description Step Name Step Description Expected Result PU Regr\Component\Attribut ...
- Spring.NET学习笔记6——依赖注入(应用篇)
1. 谈到高级语言编程,我们就会联想到设计模式:谈到设计模式,我们就会说道怎么样解耦合.而Spring.NET的IoC容器其中的一种用途就是解耦合,其最经典的应用就是:依赖注入(Dependeny I ...
- paxos 分布一致性算法的一些资料
http://blog.csdn.net/russell_tao/article/details/7238783 技术牛人博客 三国背景来讲述分布一致性算法 此人还是NGINX的代码解析 <深入 ...
- SVN中检出 和 导出 的区别
SVN中检出 和 导出 的区别:检出得到的文件夹中,是受SVN客户端控制的,对其进行文件或文件夹的增删改操作都会被SVN客户端识别出来,对其可以进行update.commit操作.其中含有.svn隐藏 ...
- bootstrap中让图片自适应不同的分辨率的方法
boostrap中加上这个样式class="img-responsive"图片就可以自适应,手机端同样适用 详细介绍请查看全文:https://cnblogs.com/qianzf ...
- Django入门与实践-第22章:基于类的视图
http://127.0.0.1:8000/boards/1/topics/2/posts/2/edit/ http://127.0.0.1:8000/ #boards/views.py from d ...
- win7-64bit下安装Scipy
一直用MAC写python,但京东给的本装的是win7系统,在安装scipy时各种报错,最后错误提示为: no lapack/blas resources found 开始一顿搜,爆栈给出的解决方案是 ...
- CodeForces 611C New Year and Domino (动态规划,DP)
题意:给定一个h*w的网格,里面只有.和#,.表示空的,#表示禁止的,然后有q个询问,询问中给你两个坐标,分别是左上和右下,求在这两者中间的有多少种(竖着和横着)两个相邻的点. 析:一看到这个题目,肯 ...
- Python 运行 Python hello.py 出错,提示: File "<stdin>" , line 1
写了一个hello.py,仅有一句,print 'hello world', 运行 Python hello.py 出错,提示: File "<stdin>" , li ...
- (匹配 匈牙利)棋盘游戏 -- Hdu --1281
链接: http://acm.hdu.edu.cn/showproblem.php?pid=1281 http://acm.hust.edu.cn/vjudge/contest/view.action ...