类加载器
上一篇文章介绍了Java 类加载机制,文中说过,类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现。
也就是说应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称 : “类加载器” 。
jvm并没有指明需要从Class文件中获取,也可以通过ZIP包、网络、动态代理方式或者是其它文件数据库等资源类型中获取二进制字节流。
类加载器虽然只用于实现类的加载动作。但是在Java中确实举足轻重的。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么两个类必定就不相等(equals()
方法,isAssignableForm()方法
,isInstance()
方法的返回结果)。
例如下面我们自定义类加载器,并重写loadClass()
方法。
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class cls = null;
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)+ ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
cls = defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return cls;
}
}
package test;
public class TestClassLoader {
public static void main(String[] args) throws Exception {
MyClassLoader myLoader = new MyClassLoader();
Object obj = myLoader.loadClass("test.TestClassLoader").newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass().getClassLoader());
System.out.println(obj instanceof test.TestClassLoader);
}
}
//class test.TestClassLoader
//test.MyClassLoader@6d06d69c
//false
上面的代码中我们使用MyClassLoader类加载器,并实例化了TestClassLoader对象。从结果中可以看出不同类加载器加载的相同类并不会相等。
虽然都来自同一个Class文件,但是仍然是两个独立的类。
双亲委派模型
像上面的示例代码其实是破坏了类加载器的双亲委派模型。
从Java开发人员的角度来看,类加载器一般分为以下3种系统提供的类加载器 :
启动类加载器(Bootstrap ClassLoader)
负责将放在<JAVA_HOME>\lib
目录中的,或被-Xbootclasspath参数
所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar)类库加载到虚拟机内存中。
启动类加载器无法被Java程序直接引用,用户再编写自定义加载器时,如果需要把加载器请求委派给启动类加载器,那么直接使用null代替即可。
扩展类加载器(Extension ClassLoader)
由sun.misc.Launcher$ExtClassLoader
实现,它负责加载在<JAVA_HOME>\lib\ext
目录中的,或被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)
由sun.misc.Launcher$App-ClassLoader
实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()
方法的返回值。所以一般也称它为系统类加载器。
它负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
上图展示的是类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
这里的类加载器之间的关系犹如父子关系(双亲),一般都是使用组合关系来复用父类加载器的代码。
该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
- 双亲委派模型的工作流程是 :
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(也可以说是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。
例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
双亲委派模型的实现代码如下 :
//ClassLoader
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// ①首先,检查请求的类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);//②委派父类加载
} else {
c = findBootstrapClassOrNull(name);//使用启动类加载器
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出异常
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// ④在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义类加载器
因为JVM并没有规定类加载方式,有许多其它方式可以获取类文件。除了简单地从本地或网络装入文件以外,可以使用定制的 ClassLoader 完成以下任务 :
- 在执行非置信代码之前,自动验证数字签名
- 动态地创建符合用户特定需要的定制化构建类
- 从特定的场所取得java class
并且双亲委派模型并不是一个强制性的约束模型,所以当使用自定义类加载器并在loadClass()
方法的逻辑里不使用父类加载器而是调用自己的方法来完成加载,那么就可以破坏双气委派模型。
首先我们要了解一下方法 :
- loadClass()
Class loadClass( String name, boolean resolve );
//name : 指定 JVM 需要的类的名称,该名称以包表示法表示,如 Foo 或 java.lang.Object。
//resolve : 是否需要解析类。在准备执行类之前,应考虑类解析。并不总是需要解析。
defineClass()
该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。findSystemClass()
从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。resolveClass()
当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值。
可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。findLoadedClass()
当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法。
下面我们来了解一下一般的自定义类加载器loadClass()
方法的实现流程 :
- 调用
findLoadedClass
来查看是否存在已装入的类。 - 如果不存在的话,通过自己的方式去获取类的字节文件。
- 如果获取到字节文件,那么转化为数组,并且调用
defineClass
将它们转换成 Class 对象。 - 如果没有获取到字节文件,调用
findSystemClass
查看是否能从本地文件系统获取类。 - 如果 resolve 参数是 true,那么调用
resolveClass
解析 Class 对象。 - 如果还没有类,返回 ClassNotFoundException。
- 解析成功,将类返回给调用程序。