简介
一个完整的Java程序是由多个.class文件组成,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用,而ClassLoader就是负责加载.class文件的。
何时被加载
Java程序启动时,不会一次性加载程序中所有的.class文件,而是在运行过程中动态的加载相应的类到内存中;
通常情况下,.class文件会在以下两种情况被ClassLoader主动加载到内存中:
调用类构造器;
调用类中的static变量或者静态方法;
ClassLoader分类
JVM中自带3个类加载器,他们在JVM中分工不同,但互相依赖。
1、启动类加载器BootstrapClassLoader*
由C/C++语言编写,本身属于虚拟机的一部分,无法在Java代码中直接获取它的引用,BootstrapClassLoader加载系统属性 “sun.boot.class.path” 配置下的类文件
System.out.println(System.getProperty("sun.boot.class.path"));
打印结果:JRE目录下的jar包或者.class文件
E:\JAVA\jdk1.8.0_221\jre\lib\resources.jar;
E:\JAVA\jdk1.8.0_221\jre\lib\rt.jar;
E:\JAVA\jdk1.8.0_221\jre\lib\sunrsasign.jar;
E:\JAVA\jdk1.8.0_221\jre\lib\jsse.jar;
E:\JAVA\jdk1.8.0_221\jre\lib\jce.jar;
E:\JAVA\jdk1.8.0_221\jre\lib\charsets.jar;
E:\JAVA\jdk1.8.0_221\jre\lib\jfr.jar;
E:\JAVA\jdk1.8.0_221\jre\classes
2、扩展类加载器ExtClassLoader
ExtClassLoader加载系统属性 “java.ext.dirs” 配置下的类文件;
System.out.println(System.getProperty("java.ext.dirs"));
打印结果:E:\JAVA\jdk1.8.0_221\jre\lib\ext;C:\windows\Sun\Java\lib\ext
3、系统加载器AppClassLoader;
面向用户类加载器,用于加载自己编写的代码以及使用的第三方jar包;
System.out.println(System.getProperty("java.class.path"));//查看AppClassLoader加载的类
打印结果:E:\JavaProject\Test1\build\classes
双亲委派模式
如何知道使用哪一个类加载去加载相应的类,就要用到双亲委派模式。双亲委派模式就是当类加载器收到加载类或资源的请求时,通常是先委托给父类加载器加载,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。
其具体实现代码在ClassLoader.java中的loadClass方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//判断该Class是否已经加载,如果已加载则直接将该Class返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果该Class没有被加载过,则判断parent是否为空,如果不为空则将加载的任务委托给parent
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果parent为空,则直接调用BootstrapClassLoader加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//如果还是没有成功,则调用当前ClassLoader的findClass方法继续尝试加载
c = findClass(name);
// 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;
}
}
每一个ClassLoader中都有一个ClassLoader类型的parent引用,并且在构造器中传入parent的值。继续深入源码可知,AppClassLoader传入的parent就是ExtClassLoader,而ExtClassLoader没有传入任何parent,即为null。
举个栗子
Tset test = new Test();
- 默认情况下,JVM首先使用AppClassLoader加载Test类;
- AppClassLoader将加载的任务委派给它的父类加载器(ExtClassLoader);
- ExtClassLoader的parent为null,所以直接将加载任务委派给BootstrapClassLoader;
- BootstrapClassLoader在jdk/lib目录下无法找到Test类,因此返回null;
- 因为parent和BootstrapClassLoader都没有成功加载Test类,所以AppClassLoader调用自身的findClass加载Test;
- 最终Test类被AppClassLoader加载到内存中;
如果想保持双亲委派模式,应该重写findClass(name)方法,如果想破坏双亲委派模式,可以重写loadClass(name)方法。
自定义ClassLoader
JVM预置的3种ClassLoader只能加载特定目录下的.class文件,如果想加载其他特殊位置下的.jar包或类时(如网络或磁盘上的.class文件),默认的ClassLoader就不能满足需求了,所以需要通过自定义ClassLoader加载.class文件。
步骤:
- 自定义一个类继承抽象类ClassLoader;
- 重写findClass方法;
- 在findClass中,调用defineClass方法将字节码转换成Class对象,并返回。
Android中的ClassLoader
Android和传统的JVM一样,需要通过ClassLoader将目标加载到内存中,类加载器之间也符合双亲委派模式,但Android中的ClassLoader的加载细节还是有略微差别。
Android虚拟机无法直接运行.class文件,Android会将所有.class文件转换成一个.dex文件,并且Android将加载.dex文件的实现封装在BaseDexClassLoader中,一般使用它的两个子类:PathClassLoader和DexClassLoader。
PathClassLoader
用来加载系统apk和被安装到手机中apk内的dex文件,构造函数如下:
//dexPath:dex文件路径,或者包含dex文件的jar包路径,一般是已经安装应用的apk文件路径
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
//librarySearchPath:C/C++ native库的路径
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
PathClassLoader只有两个构造方法,具体实现在BaseDexClassLoader里。当一个App被安装到手机后,apk里面的class.dex中的class均是通过PathClassLoader来加载的,可以通过代码验证;
ClassLoader loader = MainActivity.class.getClassLoader();
System.out.println(loader.toString());
打印结果如下:
dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/com.gree.smartcarddemo-Oc5Bm8N4ZFvwIUNapQAacQ==/base.apk”],
nativeLibraryDirectories=[/data/app/com.gree.smartcarddemo-Oc5Bm8N4ZFvwIUNapQAacQ==/lib/arm64, /data/app/com.gree.smartcarddemo-Oc5Bm8N4ZFvwIUNapQAacQ==/base.apk!/lib/arm64-v8a, /system/lib64, /product/lib64]]]
DexClassLoader
DexClassLoader可以从SD卡上加载包含class.dex的.jar和.apk文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,加载需要使用的dex文件。构造函数如下:
//dexPath:包含class.dex的apk、jar文件路径,多个路径用文件分隔符分隔
//optimizedDirectory用来缓存优化dex文件的路径,即从apk或jar文件中提取出来的dex文件,该路径不能为空
//且应该是私有的,有读写权限的路径
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}