Android 解析 ClassLoader 系列
前言
热修复和插件化是目前比较热门的技术, 要想更好的掌握它们需要了解 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"));
- }
- }
打印结果为:
- 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
可以发现几乎都是 $JAVA_HOME/jre/lib 目录中的 jar 包, 包括 rt.jarresources.jar 和 charsets.jar 等等
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"));
打印结果为:
- C: \Program Files\Java\jdk1.8.0_102\jre\lib\ext;
- C: \Windows\Sun\Java\lib\ext
- 1.3 App ClassLoader
负责加载当前应用程序 Classpath 目录下的所有 jar 和 Class 文件也可以加载通过 - Djava.class.path 选项所指定的目录下的 jar 和 Class 文件
1.4 Custom ClassLoader
除了系统提供的类加载器, 还可以自定义类加载器, 自定义类加载器通过继承 java.lang.ClassLoader 类的方式来实现自己的类加载器, 除了 Bootstrap ClassLoader,Extensions ClassLoader 和 App ClassLoader 也继承了 java.lang.ClassLoader 类关于自定义类加载器后面会进行介绍
2.ClassLoader 的继承关系
运行一个 Java 程序需要用到几种类型的类加载器呢? 如下所示
- 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();
- }
- }
- }
首先我们得到当前类 ClassLoaderTest 的类加载器, 并在注释 1 处打印出来, 接着打印出当前类的类加载器的父加载器, 直到没有父加载器终止循环打印结果如下所示
- sun.misc.Launcher$AppClassLoader@75b84c92
- sun.misc.Launcher$ExtClassLoader@1b6d3586
第 1 行说明加载 ClassLoaderTest 的类加载器是 AppClassLoader, 第 2 行说明 AppClassLoader 的父加载器为 ExtClassLoader 至于为何没有打印出 ExtClassLoader 的父加载器 Bootstrap ClassLoader, 这是因为 Bootstrap ClassLoader 是由 C/C++ 编写的, 并不是一个 Java 类, 因此我们无法在 Java 代码中获取它的引用
我们知道系统所提供的类加载器有 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 方法, 如下所示
- 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;
- }
- }
注释 1 处用来检查类是否已经加载, 如果已经加载则后面的代码不会执行, 最后会返回该类没有加载则会接着向下执行
注释 2 处, 如果父类加载器不为 null, 则调用父类加载器的 loadClass 方法如果父类加载器为 null 则调用注释 3 处的 findBootstrapClassOrNull 方法, 这个方法内部调用了 Native 方法 findBootstrapClass,findBootstrapClass 方法中最终会用 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 文件, 如下所示
- package com.example;
- public class Jobs {
- public void say() {
- System.out.println("One more thing");
- }
- }
将这个 Jobs.java 放入到 D:\lib 中, 使用 cmd 命令进入 D:\lib 目录中, 执行 Javac Jobs.java 对该 java 文件进行编译, 这时会在 D:\lib 中生成 Jobs.class
4.2 编写自定义 ClassLoader
接下来编写自定义 ClassLoader, 如下所示
- 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";
- }
- }
- }
这段代码有几点需要注意的, 注释 1 处的 loadClassData 方法会获得 class 文件的字节码数组, 并在注释 2 处调用 defineClass 方法将 class 文件的字节码数组转为 Class 类的实例 loadClassData 方法中需要对流进行操作, 关闭流的操作要放在 finally 语句块中, 并且要对 in 和 out 分别采用 try 语句, 如果 in 和 out 共同在一个 try 语句中, 那么如果 in.close()发生异常, 则无法执行 out.close()
最后我们来验证 DiskClassLoader 是否可用, 代码如下所示
- 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();
- }
- }
- }
注释 1 出创建 DiskClassLoader 并传入要加载类的路径, 注释 2 处加载 Class 文件, 需要注意的是, 不要在项目工程中存在名为 com.example.Jobs 的 Java 文件, 否则就不会使用 DiskClassLoader 来加载, 而是 AppClassLoader 来负责加载, 这样我们定义 DiskClassLoader 就变得毫无意义接下来在注释 3 通过反射来调用 Jobs 的 say 方法, 打印结果如下:
- com.example.DiskClassLoader@4554617c
- One more thing
使用了 DiskClassLoader 来加载 Class 文件, say 方法也正确执行, 显然我们的目的达到了
后记
这一篇文章我们学习了 Java 中的 ClassLoader, 包括 ClassLoader 的类型双亲委托模式 ClassLoader 继承关系以及自定义 ClassLoader, 为的是就是更好的理解下一篇所要讲解的 Android 中的 ClassLoader
参考资料
一看你就懂, 超详细 java 中的 ClassLoader 详解
深入分析 Java ClassLoader 原理
来源: http://liuwangshu.cn/application/classloader/1-java-classloader-.html