类加载器 ClassLoader
在 Java 内存模型 我们介绍了 Java 字节码文件(.class)的格式。一个完整的 Java 程序是由多个 .class 文件组成的,在程序运行过程中,需要将这些 .class 文件加载到 JVM 中才可以使用。而负责加载这些 .class 文件的就是类加载器(ClassLoader)。
Java 中的类何时被加载器加载
在 Java 程序启动的时候,并不会一次性加载程序中所有的 .class 文件,而是在程序的运行过程中,动态地加载相应的类到内存中。
通常情况下,Java 程序中的 .class 文件会在以下 2 种情况下被 ClassLoader 主动加载到内存中:
- 调用类构造器
- 调用类中的静态(static)变量或者静态方法
Java 中 ClassLoader
JVM 中自带 3 个类加载器:
- 启动类加载器 BootstrapClassLoader
- 扩展类加载器 ExtClassLoader (JDK 1.9 之后,改名为 PlatformClassLoader)
- 系统加载器 APPClassLoader
以上 3 者在 JVM 中有各自分工,但是又互相有依赖。
类加载机制
类的加载指将编译好的 Class 类文件中的字节码读入内存中,将其放在方法区内并创建对应的 Class 对象。类的加载分为加载、链接、初始化,其中链接又包括验证、准备、解析三步。如下图所示。
- 加载是文件到内存的过程。通过类的完全限定名查找此类字节码文件,并利用字节码文件创建一个 Class 对象。
- 验证是对类文件内容验证。目的在于确保 Class 文件符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种:文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备阶段是进行内存分配。为类变量也就是类中由 static 修饰的变量分配内存,并且设置初始值。这里要注意,初始值是 0 或者 null,而不是代码中设置的具体值,代码中设置的值是在初始化阶段完成的。另外这里也不包含用 final 修饰的静态变量,因为 final 在编译的时候就会分配。
- 解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量等。
- 初始化,主要完成静态块执行与静态变量的赋值。这是类加载最后阶段,若被加载类的父类没有初始化,则先对父类进行初始化。
只有对类主动使用时,才会进行初始化,初始化的触发条件包括在创建类的实例时、访问类的静态方法或者静态变量时、Class.forName() 反射类时、或者某个子类被初始化时。
如上图所示,浅绿的两个部分表示类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被 GC 卸载回收。这里要注意一点,由 Java 虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。
类加载器
如上图所示,Java 自带的三种类加载器分别是:BootStrap 启动类加载器、扩展类加载器和应用加载器(也叫系统加载器)。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载 java home 中 lib 目录下的类,扩展加载器负责加载 ext 目录下的类,应用加载器加载 classpath 指定目录下的类。除此之外,可以自定义类加载器。
BootstrapClassLoader 启动类加载器
它并不是使用 Java 代码实现的,而是由 C/C++ 语言编写的,它本身属于虚拟机的一部分。因此我们无法在 Java 代码中直接获取它的引用。
Bootstrap类加载器负责加载rt.jar 中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果尝试在 Java 层获 BootstrapClassLoader 的引用,系统会返回 null,任何基于此的代码会抛出NullPointerException 异常。Bootstrap加载器被称为初始类加载器。
ExtClassLoader 扩展类加载器
而Extension将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader 实现
APPClassLoader 系统类加载器
默认的加载器就是System类加载器(又叫作Application类加载器)了。它负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath 属性。Application类加载器是Extension类加载器的子加载器
双亲委派模式(Parents Delegation Model)
既然 JVM 中已经有了这 3 种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案就是:双亲委派模式。
所谓双亲委派模式就是,当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。
注意:"双亲委派"机制只是 Java 推荐的机制,并不是强制的机制。我们可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。
Custom ClassLoader
JVM 中预置的 3 种 ClassLoader 只能加载特定目录下的 .class 文件,如果我们想加载其他特殊位置下的 jar 包或类时(比如,我要加载网络或者磁盘上的一个 .class 文件),默认的 ClassLoader 就不能满足我们的需求了,所以需要定义自己的 Classloader 来加载特定目录下的 .class 文件。
自定义 ClassLoader 步骤:
- 自定义一个类继承抽象类 ClassLoader。
- 重写 findClass 方法。
- 在 findClass 中,调用 defineClass 方法将字节码转换成 Class 对象,并返回。
public class DiskClassLoader extends ClassLoader{ String filePath; public DiskClassLoader(String filePath){ this.filePath = filePath; } @RequiresApi(api = Build.VERSION_CODES.O) @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = filePath+name+".class"; byte [] classBytes = null; try { classBytes = Files.readAllBytes(Paths.get(new URI(path))); } catch (IOException e) { e.printStackTrace(); } catch (URISyntaxException e) { e.printStackTrace(); } return defineClass(name,classBytes,0,classBytes.length); } }
注意:如没有特殊的要求,一般不建议重写loadClass搜索类的算法。
加载器小结
Java 的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,如上图中蓝色向上的箭头。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。如图中的桔黄色向下的箭头。
这种双亲委派模式的好处,可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。
Android 中的 ClassLoader
本质上,Android 和传统的 JVM 是一样的,也需要通过 ClassLoader 将目标类加载到内存,类加载器之间也符合双亲委派模型。但是在 Android 中, ClassLoader 的加载细节有略微的差别。
在 Android 虚拟机里是无法直接运行 .class 文件的,Android 会将所有的 .class 文件转换成一个 .dex 文件,并且 Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。
DexClassLoader
先来看官方对 DexClassLoader 的描述:
A class loader that loads classes from {@code .jar} and {@code .apk} files containing a {@code classes.dex} entry. This can be used to execute code not installed as part of an application.
DexClassLoader 可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。
DexClassLoader 的源码里面只有一个构造方法,代码如下:
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:此参数已弃用,自 API 级别 26 起无效。
- librarySearchPath:C/C++ native 库的路径,多个路径用文件分隔符分隔; 可能是null。
- parent:父类加载器
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!"); } @SystemApi(client = MODULE_LIBRARIES) public PathClassLoader( @NonNull String dexPath, @Nullable String librarySearchPath, @Nullable ClassLoader parent, @Nullable ClassLoader[] sharedLibraryLoaders) { super(dexPath, librarySearchPath, parent, sharedLibraryLoaders); } }
参数说明:
- dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;
- librarySearchPath:C/C++ native 库的路径,多个路径用文件分隔符分隔; 可能是null。
- parent:父类加载器
PathClassLoader 里面除了上面这些以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。
当一个 App 被安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的,可以通过如下代码验证:
public class MainActivity extends ActivityBase { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); MLog.e(this.getClass().getName(), "onCreate"); setContentView(R.layout.activity_main); ClassLoader classLoader = MainActivity.class.getClassLoader(); MLog.e(classLoader.toString()); } }
打印结果如下:
2021-09-26 17:55:56.530 /com.scc.demo E/-SCC-com.scc.demo.actvitiy.MainActivity: onCreate 2021-09-26 17:55:56.770 /com.scc.demo E/-SCC-: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.scc.demo-0huSvtxqzKDw3GXvCu3P8g==/base.apk"], nativeLibraryDirectories=[ /data/app/com.scc.demo-0huSvtxqzKDw3GXvCu3P8g==/lib/arm64, /data/app/com.scc.demo-0huSvtxqzKDw3GXvCu3P8g==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]]
小结
- ClassLoader 就是用来加载 class 文件的,不管是 jar 中还是 dex 中的 class。
- Java 中的 ClassLoader 通过双亲委托来加载各自指定路径下的 class 文件。
- 可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法。
- Android 中常用的两种 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。