----------------------
android培训、java培训、期待与您交流! -------------------------
类加载器
概念:类加载器(class loader)用来加载 Java类到 Java
虚拟机中。一般来说,Java虚拟机使用 Java类的方式如下:Java
源程序(.java文件)在经过 Java
编译器编译之后就被转换成 Java字节代码(.class
文件)。类加载器负责读取 Java字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如
Java字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
类加载器几个注意事项:
1.java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,
ExtClassLoader , AppClassLoader
2.类加载器也是java类,因为其他是java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这就是BootStrap(它是用C++编写的)
3.java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类加载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。
类加载器优先级:BootStrap > ExtClassLoader > AppClassLoader
类加载器的树状组织结构
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由
Java应用开发人员编写的。
系统提供的类加载器主要有下面三个:
-
引导类加载器(bootstrap class loader):它用来加载
Java 的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。 -
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载
Java类。 -
系统类加载器(system class loader):它根据Java
应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器
Java 类的类加载器。因为类加载器 Java类如同其它的 Java类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。
类加载器树状组织结构示意图:
小提示:基本上所有的类加载器都是java.lang.ClassLoader
类的一个实例
java.lang.ClassLoader类介绍
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载
Java应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法。
ClassLoader 中与加载类相关的方法
方法 |
说明 |
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类,返回的结果是 |
resolveClass(Class<?> c) |
链接指定的 Java 类。 |
演示类加载器的树状组织结构
示例代码:
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(); } } }
类加载器的委托加载机制:
1.当java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类,如果类A中引用了类B,java虚拟机将使用加载类A的加载器来加载类B,还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
2.每个类加载器加载类时,又先委托给其上级类加载器。
当所有祖宗类加载器没有加载到类时,回到发起者类加载器,还加载不了,则抛出ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild方法。
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; publicvoid 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 = newFileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = newFileSystemClassLoader(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
atsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
atjava.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
atclassloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException:com.example.Sample
cannot be cast to com.example.Sample
atcom.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类空间。这种技术在许多框架中都被用到。
加载类的过程
在前面介绍类加载器的委托加载机制的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining
loader),后者称为初始加载器(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类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass方法不会被重复调用。
Class.forName
Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(Stringname, boolean initialize, ClassLoader
loader)和Class.forName(String className)。第一种形式的参数name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数initialize的值为true,loader的值为当前类的类加载器。Class.forName的一个很常见的用法是在加载数据库驱动的时候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载
Apache Derby 数据库的驱动。
开发自己的类加载器
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在
Java虚拟机中运行的类来。
java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写loadClass()方法,而是覆写findClass()方法。
类FileSystemClassLoader的findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成java.lang.Class类的实例。
总结
类加载器是 Java语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与
Web容器和 OSGi
的关系等。开发人员在遇到ClassNotFoundException和NoClassDefFoundError等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。
----------------------
android培训、java培训、期待与您交流! -------------------------