前言
热修复和插件化是目前比较热门的技术,要想更好的掌握它们需要了解 ClassLoader,因此也就有了本系列的产生,这一篇我们先来学习 Java 中的 ClassLoader.
1.ClassLoader 的类型
在 Java 虚拟机(一)结构原理与运行时数据区域 这篇文章中,我提到过类加载子系统,它的主要作用就是通过多种类加载器(ClassLoader)来查找和加载 Class 文件到 Java 虚拟机中.
Java 中的类加载器主要有两种类型,系统类加载和自定义类加载器.其中系统类加载器包括 3 种,分别是 Bootstrap ClassLoader, Extensions ClassLoader 和 App ClassLoader.
1.1 Bootstrap ClassLoader
用 C/C++ 代码实现的加载器,用于加载 Java 虚拟机运行时所需要的系统类,如
java.lang. * ,java.uti. *
等这些系统类,它们默认在 $JAVA_HOME/jre/lib 目录中,也可以通过启动 Java 虚拟机时指定 - Xbootclasspath 选项,来改变 Bootstrap ClassLoader 的加载目录.
Java 虚拟机的启动就是通过 Bootstrap ClassLoader 创建一个初始类来完成的.由于 Bootstrap ClassLoader 是使用 C/C++ 语言实现的, 所以该加载器不能被 Java 代码访问到.需要注意的是 Bootstrap ClassLoader 并不继承 java.lang.ClassLoader.
我们可以通过如下代码来得出 Bootstrap ClassLoader 所加载的目录:
打印结果为:
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
可以发现几乎都是 $JAVA_HOME/jre/lib 目录中的 jar 包,包括 rt.jar,resources.jar 和 charsets.jar 等等.
C: \Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C: \Program Files\Java\jdk1.8.0_102\jre\classes
1.2 Extensions ClassLoader
用于加载 Java 的拓展类 ,拓展类的 jar 包一般会放在 $JAVA_HOME/jre/lib/ext 目录下,用来提供除了系统类之外的额外功能.也可以通过 - Djava.ext.dirs 选项添加和修改 Extensions ClassLoader 加载的路径.
通过以下代码可以得到 Extensions ClassLoader 加载目录:
System.out.println(System.getProperty("java.ext.dirs"));
打印结果为:
负责加载当前应用程序 Classpath 目录下的所有 jar 和 Class 文件.也可以加载通过 - Djava.class.path 选项所指定的目录下的 jar 和 Class 文件.
C: \Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C: \Windows\Sun\Java\lib\ext
1.3 App ClassLoader
1.4 Custom ClassLoader
除了系统提供的类加载器,还可以自定义类加载器,自定义类加载器通过继承 java.lang.ClassLoader 类的方式来实现自己的类加载器,除了 Bootstrap ClassLoader,Extensions ClassLoader 和 App ClassLoader 也继承了 java.lang.ClassLoader 类.关于自定义类加载器后面会进行介绍.
2.ClassLoader 的继承关系
运行一个 Java 程序需要用到几种类型的类加载器呢?如下所示.
首先我们得到当前类 ClassLoaderTest 的类加载器,并在注释 1 处打印出来,接着打印出当前类的类加载器的父加载器,直到没有父加载器终止循环.打印结果如下所示.
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while (loader != null) {
System.out.println(loader); //1
loader = loader.getParent();
}
}
}
第 1 行说明加载 ClassLoaderTest 的类加载器是 AppClassLoader,第 2 行说明 AppClassLoader 的父加载器为 ExtClassLoader.至于为何没有打印出 ExtClassLoader 的父加载器 Bootstrap ClassLoader,这是因为 Bootstrap ClassLoader 是由 C/C++ 编写的,并不是一个 Java 类,因此我们无法在 Java 代码中获取它的引用.
sun.misc.Launcher$AppClassLoader@75b84c92
sun.misc.Launcher$ExtClassLoader@1b6d3586
我们知道系统所提供的类加载器有 3 种类型,但是系统提供的 ClassLoader 相关类却不只 3 个.另外,AppClassLoader 的父类加载器为 ExtClassLoader,并不代表 AppClassLoader 继承自 ExtClassLoader,ClassLoader 的继承关系如下所示.
可以看到上图中共有 5 个 ClassLoader 相关类,下面简单对它们进行介绍:
ClassLoader 是一个抽象类,其中定义了 ClassLoader 的主要功能.
SecureClassLoader 继承了抽象类 ClassLoader,但 SecureClassLoader 并不是 ClassLoader 的实现类,而是拓展了 ClassLoader 类加入了权限方面的功能,加强了 ClassLoader 的安全性.
URLClassLoader 继承自 SecureClassLoader,用来通过 URl 路径从 jar 文件和文件夹中加载类和资源.
ExtClassLoader 和 AppClassLoader 都继承自 URLClassLoader,它们都是 Launcher 的内部类,Launcher 是 Java 虚拟机的入口应用,ExtClassLoader 和 AppClassLoader 都是在 Launcher 中进行初始化的.
3 双亲委托模式
3.1 双亲委托模式的特点
类加载器查找 Class 所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该 Class 是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的 Bootstrap ClassLoader,如果 Bootstrap ClassLoader 找到了该 Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找.
这样讲可能会有些抽象,来看下面的图.
我们知道类加载子系统用来查找和加载 Class 文件到 Java 虚拟机中,假设我们要加载一个位于 D 盘的 Class 文件,这时系统所提供的类加载器不能满足条件,这时就需要我们自定义类加载器继承自 java.lang.ClassLoader,并复写它的 findClass 方法.加载 D 盘的 Class 文件步骤如下:
自定义类加载器首先从缓存中要查找 Class 文件是否已经加载,如果已经加载就返回该 Class,如果没加载则委托给父加载器也就是 App ClassLoader.
按照上图中红色虚线的方向递归步骤 1.
一直委托到 Bootstrap ClassLoader,如果 Bootstrap ClassLoader 在缓存中还没有查找到 Class 文件,则在自己的规定路径 $JAVA_HOME/jre/libr 中或者 - Xbootclasspath 选项指定路径的 jar 包中进行查找,如果找到则返回该 Class,如果没有则交给子加载器 Extensions ClassLoader.
Extensions ClassLoader 查找 $JAVA_HOME/jre/lib/ext 目录下或者 - Djava.ext.dirs 选项指定目录下的 jar 包,如果找到就返回,找不到则交给 App ClassLoader.
App ClassLoade 查找 Classpath 目录下或者 - Djava.class.path 选项所指定的目录下的 jar 包和 Class 文件,如果找到就返回,找不到交给我们自定义的类加载器,如果还找不到则抛出异常.
总的来说就是 Class 文件加载到类加载子系统后,先沿着图中红色虚线的方向自下而上进行委托,再沿着黑色虚线的方向自上而下进行查找,整个过程就是先上后下.
类加载的步骤在 JDK8 的源码中也得到了体现,来查看抽象类的 ClassLoader 方法,如下所示.
注释 1 处用来检查类是否已经加载,如果已经加载则后面的代码不会执行,最后会返回该类.没有加载则会接着向下执行.
protected Class < ?>More...loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(getClassLoadingLock(name)) {
Class < ?>c = findLoadedClass(name); //1
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); //2
} else {
c = findBootstrapClassOrNull(name); //3
}
} catch(ClassNotFoundException e) {}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); //4
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
注释 2 处,如果父类加载器不为 null,则调用父类加载器的 loadClass 方法.如果父类加载器为 null 则调用注释 3 处的 findBootstrapClassOrNull 方法,这个方法内部调用了 Native 方法 findLoadedClass0,findLoadedClass0 方法中最终会用 Bootstrap Classloader 来查找类.如果 Bootstrap Classloader 仍没有找到该类,也就说明向上委托没有找到该类,则调用注释 4 处的 findClass 方法继续向下进行查找.
3.2 双亲委托模式的好处
采取双亲委托模式主要有两点好处:
避免重复加载,如果已经加载过一次 Class,就不需要再次加载,而是先从缓存中直接读取.
更加安全,如果不使用双亲委托模式,就可以自定义一个 String 类来替代系统的 String 类,这显然会造成安全隐患,采用双亲委托模式会使得系统的 String 类在 Java 虚拟机启动时就被加载,也就无法自定义 String 类来替代系统的 String 类,除非我们修改
类加载器搜索类的默认算法.还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java 虚拟机才会认为它们是同一个类,想要骗过 Java 虚拟机显然不会那么容易.
4. 自定义 ClassLoader
系统提供的类加载器只能够加载指定目录下的 jar 包和 Class 文件,如果想要加载网络上的或者是 D 盘某一文件中的 jar 包和 Class 文件则需要自定义 ClassLoader.
实现自定义 ClassLoader 需要两个步骤:
定义一个自定义 ClassLoade 并继承抽象类 ClassLoader.
复写 findClass 方法,并在 findClass 方法中调用 defineClass 方法.
下面我们就自定义一个 ClassLoader 用来加载位于 D:\lib 的 Class 文件.
4.1 编写测试 Class 文件
首先编写测试类并生成 Class 文件,如下所示.
将这个 Jobs.java 放入到 D:\lib 中,使用 cmd 命令进入 D:\lib 目录中,执行 Javac Jobs.java 对该 java 文件进行编译,这时会在 D:\lib 中生成 Jobs.class.
package com.example;
public class Jobs {
public void say() {
System.out.println("One more thing");
}
}
4.2 编写自定义 ClassLoader
接下来编写自定义 ClassLoader,如下所示.
这段代码有几点需要注意的,注释 1 处的 loadClassData 方法会获得 class 文件的字节码数组,并在注释 2 处调用 defineClass 方法将 class 文件的字节码数组转为 Class 类的实例.loadClassData 方法中需要对流进行操作,关闭流的操作要放在 finally 语句块中,并且要对 in 和 out 分别采用 try 语句,如果 in 和 out 共同在一个 try 语句中,那么如果 in.close() 发生异常,则无法执行 out.close().
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class DiskClassLoader extends ClassLoader {
private String path;
public DiskClassLoader(String path) {
this.path = path;
}@Override protected Class < ?>findClass(String name) throws ClassNotFoundException {
Class clazz = null;
byte[] classData = loadClassData(name); //1
if (classData == null) {
throw new ClassNotFoundException();
} else {
clazz = defineClass(name, classData, 0, classData.length); //2
}
return clazz;
}
private byte[] loadClassData(String name) {
String fileName = getFileName(name);
File file = new File(path, fileName);
InputStream in =null;
ByteArrayOutputStream out = null;
try { in =new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch(IOException e) {
e.printStackTrace();
} finally {
try {
if ( in !=null) { in .close();
}
} catch(IOException e) {
e.printStackTrace();
}
try {
if (out != null) {
out.close();
}
} catch(IOException e) {
e.printStackTrace();
}
}
return null;
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if (index == -1) { //如果没有找到'.'则直接在末尾添加.class
return name + ".class";
} else {
return name.substring(index + 1) + ".class";
}
}
}
最后我们来验证 DiskClassLoader 是否可用,代码如下所示.
注释 1 出创建 DiskClassLoader 并传入要加载类的路径,注释 2 处加载 Class 文件,需要注意的是,不要在项目工程中存在名为 com.example.Jobs 的 Java 文件,否则就不会使用 DiskClassLoader 来加载,而是 AppClassLoader 来负责加载,这样我们定义 DiskClassLoader 就变得毫无意义.接下来在注释 3 通过反射来调用 Jobs 的 say 方法,打印结果如下:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) {
DiskClassLoader diskClassLoader = new DiskClassLoader("D:\\lib");//1
try {
Class c = diskClassLoader.loadClass("com.example.Jobs");//2
if (c != null) {
try {
Object obj = c.newInstance();
System.out.println(obj.getClass().getClassLoader());
Method method = c.getDeclaredMethod("say", null);
method.invoke(obj, null);//3
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
使用了 DiskClassLoader 来加载 Class 文件,say 方法也正确执行,显然我们的目的达到了.
com.example.DiskClassLoader@4554617c
One more thing
后记
这一篇文章我们学习了 Java 中的 ClassLoader,包括 ClassLoader 的类型,双亲委托模式,ClassLoader 继承关系以及自定义 ClassLoader,为的是就是更好的理解下一篇所要讲解的 Android 中的 ClassLoader.
来源: http://www.jianshu.com/p/c54285a0095b