概述
一个完整的java程序是由多个class文件构成的,程序运行时,JVM需要将这些class加载到内存中才能使用到它们。负责加载这些class文件的是 ClassLoader。
ClassLoader加载class,并不是一次性加载所有class。而是动态按需加载,它有自己的一套加载逻辑。
加载class的时机
通常,以下两种情况下,class会被classLoader加载到JVM中。
-
调用类的构造方法
- 这个很容易理解,直接调用类的构造器创建出它的class对象
-
调用类中的静态变量或者方法
- 因为一个类的变量和方法都依赖于这个类的class对象,所以必须先构建出class对象
Java类加载器的分类
系统类加载器 AppClassLoader
它是从 当前环境变量中先读取出
java.class.path
的值(也就是classPath
的值),然后从路径中找到所有的class文件进行加载。
它是面向用户的classLoader
,我们自己的java代码,或者第三方的jar包内的java代码,都会通过它来加载到JVM。
扩展类加载器 ExtClassLoader

它则是加载的java.ext.dirs
下的所有class文件。与 AppClassLoader相比,只是取class的路径不同。
注意:在不同的jdk版本中,名称可能不同,比如在jdk1.8上,名称为:PlatformClassLoader
启动类加载器 BootstrapClassLoader
与上面两种都不同, 这个BootstrapClassLoader 并不是java代码编写的,而是c/c++写的,它本身就是JVM的一部分,如果在java中尝试获取它的引用,会返回null。
它所检索的路径 为:
System.getProperty("sun.boot.class.path");
这些路径主要指向了 JRE(java Runtime Environment java运行环境)中的jar包。
双亲委派机制
Class加载流程
在java中,已经存在了这三种ClassLoader,那么当程序运行起来时,JVM如何决定用那种类加载器去加载需要的class文件呢?毕竟他们3个所检索的 文件路径都不同。
JVM内部存在一个名叫 双亲委派模式
的机制。
当类加载器收到加载class的请求时,通常都是优先委托给父类加载器去加载。只有当父类加载器找不到指定资源或者类的时候,才会执行自身的加载过程。
每个JDK版本的 ClassLoader源代码都有可能不同,以下是 jdk18.0.1(也就是jdk1.8):

有一些细节需要注意:
- 它自带缓存机制,已经加载过的class会被保存起来,保存的位置在 native层(而不是在java层),大概可以推测 缓存的数据结构是Map类型,Key是 String类型,应该是 class的全类名字符串,value则是 Class对象

-
每次加载时,优先从缓存中获取,如果获取到了,直接返回。
-
缓存中没获取到,那就将 加载动作委托给 parent,
-
如果parent为空,则调用启动类加载器(BootstrapClassLoader)的加载过程
-
如果parent不为空,则 调用自身的findClass方法
关于Parent ClassLoader
ClassLoader的构造函数要求传入parent:

java中的3个classLoader,经过源码阅读可以发现:
-
AppClassLoader的parent是 ExtClassLoader
-
ExtClassLoader的parent是 null
示例代码如下
自定义一个Test类
java
public class Test {
}
写一个main函数,用于打印classLoader
java
public class Main {
public static void main(String[] args) {
ClassLoader c1 = Test.class.getClassLoader();
System.out.println("c1 is "+ c1);
ClassLoader c2 = c1.getParent();
System.out.println("c2 is "+ c2);
ClassLoader c3 = c2.getParent();
System.out.println("c3 is "+ c3);
}
}
打印结果为:
java
c1 is jdk.internal.loader.ClassLoaders$AppClassLoader@70dea4e
c2 is jdk.internal.loader.ClassLoaders$PlatformClassLoader@4517d9a3
c3 is null
第一个为系统类加载器AppClassLoader没错,第二个,为 扩展类加载器 $PlatformClassLoade,第三个为null
模拟加载场景
java
Test t = new Test();
比如这个场景,我们触发了Test类的构造函数,
-
AppClassLoader会将它的加载动作委托给它的 parent也就是,ExtClassLoader
-
ExtClassLoader 又会去找它的parent,发现是null
-
于是ExtClassLoader将加载动作交给了 BootstrapClassLoader
-
BootstrapClassLoader 在 jre目录中 找了一圈,发现并没有找到test
-
AppClassLoader 只能自己去加载test
自定义ClassLoader示例
JVM中的ClassLoader只能加载特定路径下的class文件,如果想加载其他位置的class文件,则需要自定义我们自己的ClassLoader。
比如我们想要自定义一个ClassLoader来加载我们硬盘上的class,步骤如下:
创建一个DistClassLoader继承 ClassLoader
java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class CustomClassLoader extends ClassLoader {
private String classpaths;
public CustomClassLoader(String classpaths) {
this.classpaths = classpaths;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] classData;
String[] paths = classpaths.split(";");
for (String path : paths) {
try {
classData = loadClassData(path, className);
return defineClass(className, classData, 0, classData.length);
} catch (IOException e) {
// 如果在当前目录路径中没有找到类,则继续尝试下一个目录路径
}
}
throw new ClassNotFoundException("Class not found: " + className);
}
private byte[] loadClassData(String classpath, String className) throws IOException {
String fileName = className.replace('.', '/') + ".class";
Path path = Paths.get(classpath, fileName);
return Files.readAllBytes(path);
}
}
在这里注意一些细节:
-
传入classPath的值就是我们需要去搜索的目录的绝对的路径,形如:
C:/Users/zwx1245985/Desktop/myClassPath/
, 如果有多个路径,那么用 英文分号;
分隔 -
自定义classLoader有两种方式,一是 仅需要重写
findClass
方法去 自定义路径下去查找我们的class文件,运行时仍然会遵循 双亲委托机制,像是 上面的写法,没有指定parent的情况下,它的parent就是 AppClassLoader(加载classpath路径) -
如果我们不想用 双亲委托机制,那么可以重写loadClass方法(本文不做案例)
-
传入多个路径的情况下,查找的顺序是 从前往后搜索,如果找到了一个,那就直接返回了。(是不是有点像 安卓mutidex热修复的?)
-
将 指定路径下的class文件转化为byte数组时,使用的是就是
nio 非阻塞文件流
创建一个普通java类
比如:
java
public class Secret {
public void printSecret() {
System.out.println("这是个秘密!");
}
}
注意 不要放在任何package内。
然后,用javac
去编译它,得到一个 Secret.class
,
把Secret.class
放到你的电脑某个路径下, 比如:C:\Users\zwx1245985\Desktop\myClassPath
开始测试
编写如下代码:
java
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
testClassLoader();
}
private static void testClassLoader() {
CustomClassLoader distClassLoader = new CustomClassLoader(
"C:/Users/zwx1245985/Desktop/myClassPath;C:/Users/zwx1245985/Desktop/myClassPath2;");
System.out.println("它的parent是:" + distClassLoader.getParent().);
try {
Class<?> c = distClassLoader.loadClass("Secret");
if (c != null) {
Object o = c.getDeclaredConstructor().newInstance();
Method method = c.getDeclaredMethod("printSecret", null);
method.invoke(o, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
-
这里我传入了多个路径,用英文分号分隔
-
使用我们自定义的classLoader尝试加载Secret.class
-
得到class之后,创建它的实例,并执行它的 printSecret方法
运行结果:
java
它的parent是:jdk.internal.loader.ClassLoaders$AppClassLoader@70dea4e
这是个秘密
它的parent是 AppClassLoader,得到验证。并且成功 打印出了 这是个秘密!
,printSecret方法被成功调用。
Andorid的ClassLoader
Andorid同样需要通过classLoader将类加载到内存,类加载器之间也是双亲委派机制。
只不过Andorid会将所有的 class转换成dex,Andorid中 加载dex的逻辑全部在BaseDexClassLoader中。
我们一般会接触它的两个子类:PathClassLoader 和 DexClassLoader
PathClassLoader
它通常用来装载 系统apk中的dex,或者 已经安装到 手机上的 app私有路径下 base.apk内的dex。
构造函数如下:
dexPath为 dex文件的路径
librarySearchPath
为 c/c++类库的搜索地址,在安卓linux环境下通常指的是 .so动态库或者.a静态库文件
验证
java
public class MainActivity extends AppCompatActivity {
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = MainActivity.class.getClassLoader();
Log.d("ClassLoaderTag", "" + classLoader);
setContentView(R.layout.activity_main);
}
}
打印结果为:
java
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/~~91P9Q_h7V9_YBN6SX6ubug==/com.zhou.gracefulpermissionframework-vwOpzcIGTQr4GnmCM8GSHQ==/base.apk"],nativeLibraryDirectories=[/data/app/~~91P9Q_h7V9_YBN6SX6ubug==/com.zhou.gracefulpermissionframework-vwOpzcIGTQr4GnmCM8GSHQ==/lib/arm64, /system/lib64, /hw_product/lib64, /system/lib64/module/multimedia, /system/product/lib64]]]
DexClassLoader
对比PathClassLoader
只能加载已安装的apk内的dex,DexClassLoader可以加载SD卡内的jar包或者apk文件,这也是插件化或者热修复的基础。
构造函数
java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(
String dexPath, // 传入自定义dex路径,多个路径默认用冒号:分隔
String optimizedDirectory, // 传入dex优化路径,不能为空
String librarySearchPath, // c/c++库搜索地址
ClassLoader parent // 父classLoader
)
{
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
-
dexPath 包含class.dex的 apk jar路径,多个路径用英文冒号
:
分隔 -
optimizedDirectory 用来缓存优化的dex路径,从apk或者jar包中提取出的dex文件会经过opt优化之后才能被classLoader加载class。 此参数必传,并且路径必须是 app的私有目录。
安卓ClassLoader热修复基本原理
DexClassLoader 是 谷歌留给开发者的一个窗口,允许我们设置自己的 类检索路径,来查找我们自定义路径下的class。
假如有这么一个接口:
java
public interface ISay {
String saySth();
}
线上app上,有这么一个 实现类,存在bug:
java
public class Say implements ISay {
@Override
public String saySth() {
return "有bug!";
}
}
我们创建了一个补丁类,也叫 Say:
java
public class Say implements ISay{
@Override
public String saySth() {
return "now is Ok!";
}
}
我们将 正确的补丁类打包成 补丁包 hotfix.jar。将它放置到 手机内存卡中。我们可以创建一个DexClassLoader
来加载它,然后执行它内部的方法。
java
public class MainActivity extends AppCompatActivity {
ISay iSay;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_jump).setOnClickListener(v -> {
String hotfixJarName = "hotfix.jar";
// 获取补丁jar的File对象
final File finalFile = new File(Environment.getExternalStorageDirectory().getPath()
+ File.separator + "" + hotfixJarName);
// 如果补丁不存在,就执行原逻辑
if (!finalFile.exists()) {
iSay = new Say();
Toast.makeText(this, "" + iSay.saySth(), Toast.LENGTH_SHORT).show();
} else {
// 如果补丁存在,那就 直接用 DexClassLoader加载它
DexClassLoader dexClassLoader = new DexClassLoader(
finalFile.getAbsolutePath(),// 检索路径
getExternalCacheDir().getAbsolutePath(), // 优化路径
null, // c/c++库检索位置
null, // 设置父classLoader为空
);
try {
// 反射创建对象,然后执行它的saySth方法
Class<?> aClass = dexClassLoader.loadClass("com.xxx.isay.Say");
ISay sayOk = (ISay) aClass.getConstructor(null).newInstance();
Toast.makeText(this, "" + sayOk.saySth(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
注意以上细节:
-
如果补丁包不存在,就走原逻辑
-
如果补丁包存在,那就创建 DexClassLoader对象,注意,此处,parent必须给null,因为 补丁的class与有bug的class是完全同名的,所以一旦我们设置 getClassLoader(它实际上是PathClassLoader,那么根据双亲委托机制,它会优先找到 原有bug的class)
-
找到 补丁class之后,反射创建对象,强转为 ISay,并执行 saySth方法
总结
-
ClassLoader是用来加载class的,无论是 dex中的还是jar中的
-
java中的三种 classLoader,分别对应了不同路径下的class,各自负责不同的区域,但是彼此之间存在父子关系
-
自定义ClassLoader时,建议重写 findClass,不建议 重写 loadClass,因为后者会破坏双亲委托机制,最好别破坏
-
Android中的两种 ClassLoader的区别,也是各自负责的区域不同,一个是写死的app私有路径,一个是支持自定义外部路径。