JVM支持两种类型类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。在此大家需要注意。从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有三个,如下所示:
- Bootstrap ClassLoader
- ExtClassLoader
- AppClassLoader
Bootstrap ClassLoader也称为启动类加载器,它由C++语言编写并嵌套在JVM内部,主要负责加载“JAVA_HOME/lib”目录中的所有类型,或者由选项“-Xbootclasspath”指定路径中的所有类型。ExtClassLoader和AppClassLoader派生于ClassLoader,并且都是采用Java语言进行编写,前者主要负责加载“JAVA_HOME/bin/ext”扩展目录中的所有类型,而后者主要加载ClassPath目录中的所有类型。
如果当前的类加载器无法满足我们的需求时,便可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。在程序中编写一个自定义类加载器是一件非常容易的事,只需要继承抽象类ClassLoader并重写findClass()方法即可。当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。
在编写一个自定义类加载器的时候,我们经常会用到ClassLoader中的一些比较常用的方法,那么接下来笔者将会围绕这些方法进行讲解。
方法名称 | 描述 |
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载类名为name的类,返回结果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 |
defineClass(String name,byte[] b,int off,int len) | 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名称为Class的已经被加载过的类,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> e) | 连接指定的一个Java类 |
在上表中,ClassLoader的getParent()方法用于获取当前类加载器的超类加载器。按照双亲委派模型的规则,除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如AppClassLoader的超类加载器就是ExtClassLoader。
一般来说,在程序中开发人员自己使用自己编写的自己编写的自定义类加载器来与默认的三种类加载器共同执行类加载任务的情况其实并不多见,但是在一些特殊的应用场景中,如果当前的类加载器无法满足我们的需求时,就需要在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。比如就像笔者之前提及过的,当一个字节码文件在编译的时候如果进行了加密处理,那么在被类加载器执行类加载操作时,首先要做的事情就是解密处理。否则类加载器就会认为这么字节码并不符合JVM规范,它不是一个有效地字节码文件。其次就是程序中如果没有显示第指定类加载器时,AppClassLoader就是任务委派的发起者。AppClassLoader主要负责加载ClassPath目录中的所有类型,但是如果被加载的类型并没有包含在ClassPath目录中时,程序最终就会抛出java.lang.ClassNotFoundException异常。为了满足这些特殊的应用场景,开发人员就需要咋程序中编写自定义类加载器。
在程序中实现一个自定义类加载器非常简单,只需要继承抽象类ClassLoader,并重写器findClass()方法即可。在此大家需要注意,尽管Java虚拟机规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器,但从严格意义上来说,由Java开发人员编写的自定义类加载器其实并不属于Java体系结构的组成部分,实际上它仅仅只是属于Java运行时程序的一部分而已。
package hanjia;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String byteCode_path;
public MyClassLoader(String path){
this.byteCode_path = path;
}
@SuppressWarnings("deprecation")
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte value[] = null;
BufferedInputStream in = null;
try{
in = new BufferedInputStream(new FileInputStream(byteCode_path+className+".class"));
value = new byte[in.available()];
in.read(value);
}catch(IOException e){
e.printStackTrace();
}finally{
if(null != in){
try{
in.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
//将byte数组转换为一个类的class对象实例
return defineClass(value,0,value.length);
}
public static void main(String[] args){
String projectPath = System.getProperty("user.dir");
MyClassLoader classLoader = new MyClassLoader(projectPath+"/");
try {
System.out.println("加载目标类的类加载器->"
+classLoader.loadClass("HeapOMM").getClassLoader().getClass().getName());
System.out.println("当前类加载器的超类加载器->"
+classLoader.getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
其中HeapOMM.class是放在项目目录中待加载的类。
输出结果为:
加载目标类的类加载器->hanjia.MyClassLoader
当前类加载器的超类加载器->sun.misc.Launcher$AppClassLoader
在上述代码中,笔者演示了如何编写一个自定义类加载器来加载指定目录中的类型,由于目标类型HeapOMM并非包含在ClassPath目录中,自然也无法被AppClassLoader执行加载。在程序中指定了加载目标类型的类加载器就是MyClassLoader,那么它就是任务委派的发起者,MyClassLoader会按照双亲委派模型将HeapOMM类的加载任务分配给超类加载器。而AppClassLoader将会按早这种方式最终委派给顶层的启动类加载器,只有当超类加载器无法执行加载时,才会将类加载任务退回给MyClassLoader执行。笔者在重写findClass()方法时,指定了以流的形式读取目标类型的二进制字节流到JVM内部,然后通过defineClass()方法将其转换为一个类的Class对象实例。
摘自《Java虚拟机精讲》高翔龙