类加载器与双亲委派模型
一 类加载器基本概念:
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。下面详细介绍这个 Java 类。
ClassLoader:
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。为了完成加载类的这个职责,ClassLoader提供了一系列的方法。
ClassLoader 中与加载类相关的方法:
- getParent() 返回该类加载器的父类加载器。
- loadClass(String name) 加载名称为 name的类,返回的结 果是java.lang.Class类的实例。
- findClass(String name) 查找名称为 name的类,返回的结果是 .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 类。
具体源码如下:
protected synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先检查该name指定的class是否有被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
} else {
// parent为null,则调用BootstrapClassLoader进行加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果仍然无法加载成功,则调用自身的findClass进行加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
二 Java虚拟机中类加载器:
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\ext,该加载器可以被开发者直接使用。
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:
三 类加载器的双亲委派加载机制:
启动(Bootstrap)类加载器----->标准扩展(Extension)类加载器--->系统(Application)类加载器---->上下文(Custom)类加载器
从左到右加载:首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
四 自定义类加载器:
自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。
在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。
被加载的类:
package com.example.test;
public class Animal {
public String say(String content){
System.out.println("say:"+content);
return "";
}
}
自定义类加载器:
package com.example.test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
class MyClassLoader extends ClassLoader {
@Override
public Class loadClass(String name) throws ClassNotFoundException {
//检查,是否该类已经加载过了,如果加载过了,就不加载了
Class c = findLoadedClass(name);
//如果未加载,则进行以下操作
if (c == null) {
//如果该类存在于当前类加载路径下,则使用自定义加载器加载,如果不存在于当前路径,则使用父类加载器
//(如加载Animal类时使用自定义,但是加载Animal也需要加载它的父类Object,这个时候就要使用父类加载器【根类加载器】)
File file = new File(getFileName(name));
if (file.exists()) {
//调用自定义的方法
return this.findClass(name);
} else {
//调用父类加载器
return super.loadClass(name);
}
} else {
return c;
}
}
@Override
public Class findClass(String name) {
System.out.println("正在使用自定义类加载器加载类:" + name);
byte[] data = loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
try {
//获取类文件的IO
FileInputStream is = new FileInputStream(new File(getFileName(name)));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = is.read()) != -1) {
baos.write(b);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//获取类文件的全路径
private String getFileName(String name) {
//将包名间隔符改为路径间隔符
name = name.replace(".", "//");
//类存放路径
String path = MyClassLoader.getSystemClassLoader().getResource("").getPath();
String fileName = path + name + ".class";
return fileName;
}
}
测试类:
package com.example.test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
MyClassLoader cl = new MyClassLoader();
Class clz = cl.loadClass("com.example.test.Animal");
Method method = clz.getMethod("say", String.class);
Object obj = clz.newInstance();
method.invoke(obj, "hello");
System.err.println("类:" + obj + ",使用的类加载器是:" + obj.getClass().getClassLoader());
}
}
运行结果:
正在使用自定义类加载器加载类:com.example.test.Animal
say:hello
类:com.example.test.Animal@a09ee92,使用的类加载器是:com.example.test.MyClassLoader@27716f4
类加载器双亲委派模型是从JDK1.2以后引入的,并且只是一种推荐的模型,不是强制要求的,因此有一些没有遵循双亲委派模型的特例:
(1).在JDK1.2之前,自定义类加载器都要覆盖loadClass方法去实现加载类的功能,JDK1.2引入双亲委派模型之后,loadClass方法用于委派父类加载器进行类加载,只有父类加载器无法完成类加载请求时才调用自己的findClass方法进行类加载,因此在JDK1.2之前的类加载的loadClass方法没有遵循双亲委派模型,因此在JDK1.2之后,自定义类加载器不推荐覆盖loadClass方法,而只需要覆盖findClass方法即可。
(2).双亲委派模式很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载,但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。
JavaEE只是一个规范,sun公司只给出了接口规范,具体的实现由各个厂商进行实现,因此JNDI,JDBC,JAXB等这些第三方的实现库就可以被JDK的类库所调用。线程上下文类加载器也没有遵循双亲委派模型。
(3).近年来的热码替换,模块热部署等应用要求不用重启java虚拟机就可以实现代码模块的即插即用,催生了OSGi技术,在OSGi中类加载器体系被发展为网状结构。OSGi也没有完全遵循双亲委派模型。