深入理解ClassLoader加载机制

ClassLoad类加载器概述

ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。

jvm启动的时候,并不会一次性加载所有的class文件,而是在程序的运行中动态的去加载。

通常Java中的.class文件会在以下两种情况被ClassLoader加载到内存中:

  1. 调用类构造器
  2. 调用类中的静态变量或静态方法

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

Java中的ClassLoader

1. BootstrapClassLoader 系统类加载器

BootstrapClassLoader并非java代码实现的,而是由C/C++代码编写的,它本身属于虚拟机的一部分,主要负责加载Java标准库的一些类,如 java.lang 包等,这些类通常位于 JDK 的 jre/lib 目录下。

2. ExtClassLoader 扩展类加载器/PlatformClassLoader 平台类加载器

负责加载Java扩展库的类,这些类通常位于JDK 的 jre/lib/ext目录下,在 Java 9 之后,由于模块化的引入,这个类加载器已经被废弃。

平台类加载器负责加载 JDK 自带的模块。这些模块通常位于 JDK 安装目录下的 jmods 文件夹中

3. APPClassLoader 系统类加载器

AppClassLoader 主要加载系统属性“java.class.path”配置下类文件,也就是环境变量 CLASS_PATH 配置的路径。因此 AppClassLoader 是面向用户的类加载器,我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

双亲委派模式(Parents Delegation Model)

既然 JVM 中已经有了这 3 种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案就是:双亲委派模式

双亲委派模式(Parent Delegation Model)是 Java 类加载器体系中的一种设计模式。它描述了类加载器在加载类时应如何与其父加载器(parent class loader)进行协作。双亲委派模式可以确保类在加载过程中的安全性和避免多次加载同一个类。

双亲委派模式的工作原理如下:

  1. 当一个类加载器(ClassLoader)被要求加载一个类时,它首先会将加载请求委托给其父类加载器。
  2. 父类加载器会递归地将加载请求继续委托给它自己的父类加载器,直到请求到达启动类加载器(Bootstrap ClassLoader)。
  3. 启动类加载器会检查它是否能够加载该类。如果可以加载,它将加载这个类并返回;否则,它会将加载请求传递回给子加载器。
  4. 子加载器会检查它是否能够加载该类。如果可以加载,它将加载这个类并返回;否则,它会将加载请求传递回给下一个子加载器。
  5. 如果所有加载器都无法加载该类,那么 ClassNotFoundException 将被抛出。

双亲委派模式的优势:

  1. 避免类的重复加载:由于每个加载器只会尝试加载它的父加载器无法加载的类,这样可以确保同一个类不会被多次加载。
  2. 保护 Java 核心类库的安全性:由于启动类加载器是第一个尝试加载类的加载器,用户自定义的类加载器无法加载 Java 核心类库中的类。这可以防止核心类库被恶意代码篡改或替换。
  3. 有助于实现类的隔离:使用不同的类加载器可以实现类的隔离,这对于应用服务器等场景非常有用,因为它们需要隔离不同应用之间的类加载。

注意:“双亲委派”机制只是 Java 推荐的机制,并不是强制的机制。我们可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。

自定义ClassLoader

JVM 中预置的 3 种 ClassLoader 只能加载特定目录下的 .class 文件,如果我们想加载其他特殊位置下的 jar 包或类时(比如,我要加载网络或者磁盘上的一个 .class 文件),默认的 ClassLoader 就不能满足我们的需求了,所以需要定义自己的 Classloader 来加载特定目录下的 .class 文件。

自定义 ClassLoader 步骤

  1. 自定义一个类继承ClassLoader
  2. 重写findClass方法
  3. 在findClass中,调用defineClass方法将字节码转换成Class对象,并且返回。

下面用一个例子来描述:

  • 首先需要写一个java文件
public class Secret {
    public void printSecret() {
        System.out.println("没想到吧 哈哈");
    }
}
  • 定一个类加载器
public class DiskClassLoader extends ClassLoader {
    @Override
    protected Class findClass(String name) {
        //.class的储存路径
        String path = "..../someWhere/";
        String classPath = path + name + ".class";
        byte[] classByte = null;
        Path p = null;
        try {
            p = Paths.get(new URI(classPath));
            classByte = Files.readAllBytes(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //本质上这个方法传入的是字节码,有操作空间
        return defineClass(name, classByte, 9, classByte.length);
    }
}
  • 调用代码
DiskClassLoader loader = new DiskClassLoader();
try {
    Class c = loader.loadClass("Secret");
    //利用反射来调用方法
    if (c != null) {
        Object obj = c.getDeclaredConstructor().newInstance();
        Method method = c.getDeclaredMethod("printSecret", null);
        method.invoke(obj, null);
    }
} catch (Exception e) {
    throw new RuntimeException(e);
}
//  最后“没想到吧 哈哈”会输出到终端

注意:上述动态加载 .class 文件的思路,经常被用作热修复和插件化开发的框架中,包括 QQ 空间热修复方案、微信 Tink 等原理都是由此而来。客户端只要从服务端下载一个加密的 .class 文件,然后在本地通过事先定义好的加密方式进行解密,最后再使用自定义 ClassLoader 动态加载解密后的 .class 文件,并动态调用相应的方法。

Android中的ClassLoader

本质上,Android 和传统的 JVM 是一样的,也需要通过 ClassLoader 将目标类加载到内存,类加载器之间也符合双亲委派模型。但是在 Android 中, ClassLoader 的加载细节有略微的差别。

在 Android 虚拟机里是无法直接运行 .class 文件的,Android 会将所有的 .class 文件转换成一个 .dex 文件,并且 Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。

BootClassLoader

BootClassLoader用来加载Android Framework层的字节码文件,Android系统启动的时候会使用BootClassLoader来预加载常用类,但是它和SDK中的Bootstrap ClassLoader不同,它并不是由C/C++代码实现的,而是由java实现的,BootClassLoader是ClassLoader的内部类,并继承自ClassLoader。BootClassLoader是一个单例类。

PathClassLoader

PathClassLoader 用来加载系统 apk 和被安装到手机中的 apk 内的 dex 文件。它的 2 个构造函数如下:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}
  • dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;
  • librarySearchPath:C/C++ native 库的路径。

PathClassLoader 里面除了这 2 个构造方法以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。当一个 App 被安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的,我们可以打印验证,这里不做演示

DexClassLoader

DexClassLoader对比PathClassLoader,可以从SD卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载,它的构造函数如下。

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}
  • dexPath:包含 class.dex 的 apk、jar 文件路径 ,多个路径用文件分隔符(默认是“:”)分隔。
  • optimizedDirectory:用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径。

很好,到这里你是不是发现了Android热修复的原理,是不是蠢蠢欲动了

使用 DexClassLoader 实现热修复

  • 创建 Android 项目 DexClassLoaderHotFix
  • ISay.java 是一个接口,内部只定义了一个方法 saySomething。
public interface ISay{
        String saySomething();
}
  • SayException.java 实现了 ISay 接口,但是在 saySomething 方法中,打印“something wrong here”来模拟一个线上的 bug。
public class SayException implements ISay{

    @Override
    public String saySomething() {
        return "something wrong here!";
    }
}
  • 最后在 MainActivity.java 中,当点击 Button 的时候,将 saySomething 返回的内容通过 Toast 显示在屏幕上。
  • 运行效果

创建 HotFix patch 包

  • 新建 Java 项目,并分别创建两个文件 ISay.java 和 SayHotFix.java。

ISay 接口的包名和类名必须和 Android 项目中保持一致。SayHotFix 实现 ISay 接口,并在 saySomething 中返回了新的结果,用来模拟 bug 修复后的结果。

  • 将 ISay.java 和 SayHotFix.java 打包成 say_something.jar,然后通过 dx 工具将生成的 say_something.jar 包中的 class 文件优化为 dex 文件。
dx --dex --output=say_something_hotfix.jar say_something.jar

上述 say_something_hotfix.jar 就是我们最终需要用作 hotfix 的 jar 包。

  • 将 HotFix patch 包拷贝到 SD 卡主目录,并使用 DexClassLoader 加载 SD 卡中的 ISay 接口

首先将 HotFix patch 保存到本地目录下。一般在真实项目中,我们可以通过向后端发送请求的方式,将最新的 HotFix patch 下载到本地中。这里为了演示,我直接使用 adb 命令将 say_somethig_hotfix.jar 包 push 到 SD 卡的主目录下:

adb push say_something_hotfix.jar /storage/self/primary/ 
  • 接下来,修改 MainActivity 中的逻辑,使用 DexClassLoader 加载 HotFix patch 中的 SayHotFix 类,如下:

注意:因为需要访问 SD 卡中的文件,所以需要在 AndroidManifest.xml 中申请权限。

总结

  • ClassLoader 就是用来加载 class 文件的,不管是 jar 中还是 dex 中的 class。
  • Java 中的 ClassLoader 通过双亲委托来加载各自指定路径下的 class 文件。
  • 可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法。
  • Android 中常用的两种 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。

QA

Q1:Java 虚拟机是如何判定两个 Java 类是相同的。

A:Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器(实例)是否一样。只有两者都相同的情况,才认为两个类是相同的。

即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

Q2:双亲委托机制的设计动机是什么?

A:是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。

1、代理模式是为了保证 Java 核心库的类型安全:通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

2、相同名称的类可以并存在 Java 虚拟机中:不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

这种技术在许多框架中都被用到,比如以 Apache Tomcat 来说,容器不希望它下面的webapps之间能互相访问到,每个 Web 应用都有一个对应的类加载器实例。

Q3:Android中的ClassLoader和Java中的ClassLoader区别

A:java中的ClassLoader本质上是加载Class文件,但是这一点在Android中并非如此,因为无论是DVM还是ART,它们加载的都不再是Class文件,而是dex文件,这就需要重新设计ClassLoader相关类。关于dex文件:Android程序一般是使用Java语言开发的,但是Dalvik虚拟机并不支持直接执行JAVA字节码,所以会对编译生成的.class文件进行翻译、重构、解释、压缩等处理,这个处理过程是由dx进行处理,处理完成后生成的产物会以.dex结尾,称为Dex文件。

你可能感兴趣的:(深入理解ClassLoader加载机制)