目录
3. BootstrapClassLoader 启动类加载器
双亲委派模式(Parents Delegation Model)
一个完整的 Java 程序是由多个 .class 文件组成的,在程序运行的过程中,需要将这些 .class 文件加载到 JVM 中才可以使用,而负责加载这些 .class 文件的就是类加载器(ClassLoader)。
Java 中的类何时被类加载器加载
Java 程序启动时,并不会一次性加载程序中所有的 .class 文件,而是在程序运行过程中,动态的加载相应的类到内存中。通常情况下,Java 程序中的 .class 文件会在以下两种情况下被 Class Loader 主动加载到内存:
1. 调用类构造器;
2. 调用类中的静态变量或者静态方法。
Java 中的 ClassLoader
JVM 中自带3个类加载器:
启动类加载器 BootstrapClassLoader;
扩展类加载器 ExtClassLoader(JDK1.9 之后,改名为 PlatformClassLoader);
系统类加载器 APPClassLoader。
1. AppClassLoader 系统类加载器
部分源码如下
可以看出 AppClassLoader 主要加载系统属性“java.class.path”配置下类文件,也就是环境变量 classpath 配置的路径下的文件。因此,AppClassLoader 是面向用户的类加载器。我们自己编写的代码以及使用的第三方 jar 包,通常都是由它来加载的。
2. ExtClassLoader 扩展类加载器
部分源码如下:
可以看出,扩展类加载器(ExtClassLoader )主要加载系统属性“java.ext.dirs”配置下类文件,可以通过如下代码打印出这个属性来查看具体有哪些文件。
System.out.println(System.getProperty("java.ext.dirs"));
3. BootstrapClassLoader 启动类加载器
BootstrapClassLoader 是由 C/C++ 语言编写的,它本身属于虚拟机的一部分,因此无法在 java 代码中直接获取它的引用。如果尝试在 Java 层获取 BootstrapClassLoader 的引用,系统会返回 null。
启动类加载器加载“sun.boot.class.path”配置下类文件,可以打印这个属性来查看具体有哪些文件
System.out.println(System.getProperty("sun.boot.class.path"));
结果如下:
可以看出这些全身 jre 目录下的的 jar 包或者 classes 文件。
双亲委派模式(Parents Delegation Model)
既然 JVM 中已经有了这3种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案这是:双亲委派模式。
双亲委派模式:当类加载器受到加载类或资源的请求时,通常都是先委托给父类加载器加载,只有当父类加载器找不到指定的类或资源时,自身才会执行实际的类加载过程。
其具体实现代码是在CLassLoader.java 中的 loadClass 方法中,如上图所示:
解释说明:上图中1处判断该 class 是否已经加载。如果已经加载,直接将该 class 返回;2处,如果该 class 没有被加载过,则判断 parent 是否为空,如果不为空则将加载的任务委托给 parent;3处,如果 parent 为空,则直接调用 BootstrapClass Loader 加载该类;4处,如果 parent 和 BootstrapClassLoader 都没有加载成功,则调用当前 ClassLoader 的 findClass() 方法继续尝试加载。
那么这个 parent 是什么呢?我们可以看 ClassLoader 的构造器,如下:
可以看出,在每一额 ClassLoader 中都有一个 ClassLoader 类型的 parent 引用,并且在构造器中传入值。继续查看源码,可以看到 AppClassLoader 传入的 parent 就是 ExtClassLoadr,而 ExtClassLoadr并没有传入任何 parent,也就是 null。
举例说明:
Test test = new Test();
默认情况下,JVM 首先调用 AppClassLoader 去加载 Test 类
1. APPClassLoader 将加载任务委派给它的父类加载器(parent)--ExtClassLoader;
2. ExtClassLoader 的 parent 为 null,则直接调用 BootstrapClassLoader 加载该类;
3. BootstrapClassLoader 在 jdk/jre 目录下无法找到 Test 类,因此返回的 Class 为 null;
4. 因为 parent 和 BootstrapClassLoader 都没能成功加载 Test 类,所以 AppClassLoader 会调用自身的 findClass() 方法来加载 Test 类。
最终,Test 就是被系统类加载器(AppClassLoader)加载到内存中的。
注意:“双亲委派”机制只是 Java 推荐的机制,并不是强制的机制。可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name)方法。
自定义 ClassLoader
JVM 中预置的3种 ClassLoader 只能加载特定目录下的 .class 文件。如果想加载其它特殊位置下的 jar 包或类时(如网络或磁盘上),默认的 ClassLoader 就不能满足我们的需求。所以需要定义自己的 ClassLoader 来加载特定目录下的 .class 文件。
自定义 ClassLoader 步骤
1. 自定义一个类(MyClassLoader)继承抽象类 ClassLoader;
2. 重写 findClass 方法;
3. 在 findClass 方法中,调用 defineClass() 方法将字节码转换成 Class 对象,并返回。
自定义 ClassLoder 实践
1. 创建一个测试类 Secret.java,实现简单的打印功能。该文件存放在 D:\HL\ 目录下。在终端通过 javac 命令生成 Secret.class 文件。
public class Secret {
public void printSecret() {
System.out.println("I am a girl!");
}
}
2. 创建 MyClassLoade 继承自 ClassLoader,重写 findClass() 方法。
public class MyClassLoader extends ClassLoader{
private String filePath;
public MyClassLoader(String path) {
filePath = path;
}
@Override
protected Class<?> findClass(String name){
// newPath = "D:\HL\Secret.class"
// File.separator 来构建文件路径,以确保在不同操作系统上都能正常工作
String newPath = filePath + File.separator + name.replace('.', File.separatorChar) + ".class";
byte[] classBytes = null;
try {
Path path = Paths.get(newPath);
classBytes = Files.readAllBytes(path);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 调用 defineClass 创建 class 并返回
return defineClass(name, classBytes, 0, classBytes.length);
}
}
3. 测试
public class TestMyClassLoader {
public static void main(String[] args) {
// 创建自定义 ClassLoader 对象
// "D:/HL/" : 需要动态加载的 class 的路径
MyClassLoader myClassLoader = new MyClassLoader("D:/HL/");
try {
//"software_test.Secret" 需要动态加载的类名
Class c = myClassLoader.loadClass("software_test.Secret");
if(c != null) {
Object obj = c.newInstance();
// 通过反射调用 Secret 的 printSecret 方法,即需要动态调用的方法名称
Method method = c.getDeclaredMethod("printSecret", null);
method.invoke(obj, null);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Android 中的 ClassLoader
本质上 Android 和传统的 JVM 是一样的,也需要 ClassLoader 将目标类加载到内存,类加载器之间也复合双亲委派模型。但是在 Android 中,ClassLoader的加载细节有略微的差别。在 Android 虚拟机里是无法直接运行 .class 文件,Android 中会将所有的 .class 文件转化为一个 .dex 文件。Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,一般我们使用它的两个子类:PathClassLoader 和 DexClassLoader。
1. PathClassLoader
用来加载系统 apk 和被安装到手机中 apk 内的 dex 文件。它的两个构造函数如下:
参数说明:
dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;
librarySearchpath:C/C++ native 库路径。
PathClassLoader 代码里除了这两个构造函数外就没有其它代码了。具体的实现都是在 BaseDexClassLoader 里面。
当一个 app 被安装到手机后,apk 里面的 class.dex 中的 .class 均是通过 PathClassLoader 来加载的。
2. DexClassLoader
对比 PathClassLoader 只能加载已经安装的应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 jar 包或者是 apk 文件。这也是插件化或热修复的基础,在不需要安装应用的情况下完成需要使用的 dex 文件的加载。
DexClassLoader 的源码里只有一个构造函数,如下:
参数说明:
dexPath:包含 class.dex 的 apk,jar 文件路径,多个路径使用文件分割符(默认是“:”)分隔;
optimizedDirectory:用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件,该路径不可以为空,且应该是私有的,有读写权限的路径。
总结
ClassLoader 是用来加载 class 文件的,不管是 jar 中还是 dex 中的 class;
Java 中的 ClassLoader 通过双亲委托来加载各自指定路径下的 class 文件;
可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法;
Android 中常用的两个 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。