目录
  1. 1. 一、ART 和 Dalvik
    1. 1.1. 1.1 Dexopt 和 DexAot
    2. 1.2. 1.2 ART 的混合编译模式(Android 7+)
  2. 2. 二、JVM 类加载器体系
    1. 2.1. 2.1 Java 类加载器层次
    2. 2.2. 2.2 双亲委托机制
    3. 2.3. 2.3 类加载锁与死锁场景
  3. 3. 三、Android 的 ClassLoader 体系
    1. 3.1. 3.1 核心类与继承关系
    2. 3.2. 3.2 PathClassLoader vs DexClassLoader
    3. 3.3. 3.3 BaseDexClassLoader 的多 DEX 加载
  4. 4. 四、热修复的类加载原理
    1. 4.1. 4.1 Tinker 方案(全量替换)
    2. 4.2. 4.2 Sophix 方案(分派热更新)
    3. 4.3. 4.3 核心实现:反射替换 dexElements
    4. 4.4. 4.4 注意事项
  5. 5. 五、Android N+ 的 ClassLoader 变化
    1. 5.1. 5.1 API 24+ 的变化
    2. 5.2. 5.2 Instant App 的 ClassLoader
  6. 6. 六、自定义 ClassLoader 实现
  7. 7. 七、Android R+ 的 InMemoryDexClassLoader 深入分析
    1. 7.1. 7.1 为什么需要内存加载?
    2. 7.2. 7.2 与 PathClassLoader/DexClassLoader 的区别
    3. 7.3. 7.3 InMemoryDexClassLoader 在热修复中的应用
  8. 8. 八、类加载死锁场景与解决方案
    1. 8.1. 8.1 经典死锁场景深入分析
    2. 8.2. 8.2 解决方案
  9. 9. 九、MultiDex 与类加载
  10. 10. 八、面试常问题目
Java进阶之深入理解ClassLoader类加载

一、ART 和 Dalvik

Dalvik 是 Google 为 Android 平台设计的 Java 虚拟机,支持已转换为 .dex(Dalvik Executable)格式的 Java 应用程序运行,.dex 格式是专为 Dalvik 设计的一种压缩格式,适合内存和处理器速度有限的系统。

ART(Android Runtime)在 Android 4.4 中作为开发者选项引入,从 Android 5.0 起成为默认运行时。ART 在应用安装时进行 AOT(Ahead-Of-Time)预编译字节码到机器语言。应用程序安装会变慢,但执行更有效率,启动更快。

  • 在 Dalvik 下,应用运行需要解释执行,常用热点代码通过 JIT 将字节码转换为机器码,运行效率较低。而在 ART 环境中,应用在安装时字节码预编译(AOT)成机器码,安装慢了但运行效率提高了。
  • ART 占用空间比 Dalvik 大(字节码变为机器码),属于”空间换时间”。
  • 预编译可以改善电池续航,因为应用程序每次运行时不用重复编译,减少了 CPU 使用频率,降低了能耗。

1.1 Dexopt 和 DexAot

ART 机制:在安装时首先对 dex 文件进行 Dexopt 验证和优化,转化为 odex 文件,再进行 AOT 提前预编译操作,编译为 AOT 可执行文件(机器码),同时兼容 Dalvik。

Dalvik VM:安装时不处理,在运行时通过 JIT 进行解释执行,其解释执行的文件为 dexopt 进行验证和优化过后的 odex(Optimized dex)文件。

1.2 ART 的混合编译模式(Android 7+)

从 Android 7.0 开始,ART 引入了同时包含 JIT 和 AOT 的混合编译模式:

首次安装 → 解释执行 + Profile 记录
→ JIT 编译热点方法(运行时编译到代码缓存)
→ 设备空闲 + 充电时 → dex2oat AOT 编译(基于 Profile 优化)
→ 下次启动直接使用 AOT 编译的机器码

Profile Guided Compilation 的关键优势:只编译真正执行的热点代码,而非整个 APK。这减少了安装时间和存储空间占用,同时保持了良好的运行时性能。

二、JVM 类加载器体系

2.1 Java 类加载器层次

JVM 使用双亲委派模型(Parent Delegation Model):

Bootstrap ClassLoader (启动类加载器)
↑ 委派
Extension ClassLoader (扩展类加载器) — JDK 9+ 改为 Platform ClassLoader
↑ 委派
Application ClassLoader (应用/系统类加载器)
↑ 委派
Custom ClassLoader (自定义类加载器)

2.2 双亲委托机制

当某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才会自己去加载。

系统源码(ClassLoader.loadClass 核心逻辑):

protected Class<?> loadClass(String name, boolean resolve) 
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查 class 是否已被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父加载器为 null 时,尝试 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器找不到,自己尝试
}
if (c == null) {
// 3. 自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

为什么需要双亲委派?

  1. 安全性:防止核心类库被篡改。例如用户自定义的 java.lang.String 不会被加载,因为 Bootstrap ClassLoader 已经加载了 rt.jar 中的 String。
  2. 唯一性:保证同一个类不会被多个 ClassLoader 重复加载。在 JVM 中,一个类的唯一标识是 (ClassLoader + 全限定名),同一个类被不同 ClassLoader 加载视为不同的类型,这将导致 ClassCastException

2.3 类加载锁与死锁场景

getClassLoadingLock(name) 返回一个对象用作类加载的同步锁。这个锁的作用是防止同一个类被多个线程并发加载——让第一个线程完成加载,其余线程直接使用已加载的结果。

但这也可能造成死锁:

// 死锁场景:两个 ClassLoader 互相加载对方的类
// ClassLoaderA 尝试加载类时需要 ClassLoaderB 的类
// ClassLoaderB 尝试加载类时需要 ClassLoaderA 的类
//
// 线程 1: ClassLoaderA.loadClass("A") → 持有 A 的锁 → 需要加载 B
// → ClassLoaderB.loadClass("B") → 等待 B 的锁
// 线程 2: ClassLoaderB.loadClass("B") → 持有 B 的锁 → 需要加载 A
// → ClassLoaderA.loadClass("A") → 等待 A 的锁
//
// 结果:死锁。这在 Android 应用启动时偶有发生,表现为 ANR

Android 的解决方式:classLoader.loadClass(name, false)resolve=false 延迟了类解析,减少了加载过程中的交叉依赖。

三、Android 的 ClassLoader 体系

3.1 核心类与继承关系

// 继承关系
ClassLoader
└── BaseDexClassLoader (abstract)
├── PathClassLoader // 加载已安装 APK 中的 DEX
├── DexClassLoader // 加载外部 JAR/APK 中的 DEX
└── InMemoryDexClassLoader // 加载内存中的 DEX (API 26+)
// BootClassLoader: 加载 Android Framework 类
// 源码: libcore/libart/src/main/java/java/lang/ClassLoader.java
class BootClassLoader extends ClassLoader {
private static BootClassLoader instance;

@FindBootstrapClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}
}

3.2 PathClassLoader vs DexClassLoader

两者都继承自 BaseDexClassLoader,在 API 26 之前的主要区别:

特性 PathClassLoader DexClassLoader
加载位置 已安装 APK (/data/app/) 任意路径的 JAR/APK
optimizedDirectory 无(使用系统默认) 需要指定(API 26+ 弃用)
用途 加载主 APK 的类 加载插件 APK、热修复补丁

从 API 26 (Android 8.0) 开始,两者的区别基本消失——都使用 ART 的 DexFile 直接在内存中加载,不再需要 optimizedDirectory

// PathClassLoader 通常由系统创建
// 应用的 ClassLoader 就是 PathClassLoader 实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

ClassLoader cl = getClassLoader();
// 输出: dalvik.system.PathClassLoader

ClassLoader parent = cl.getParent();
// 输出: java.lang.BootClassLoader
}

3.3 BaseDexClassLoader 的多 DEX 加载

// BaseDexClassLoader 内部使用 DexPathList 管理多个 DEX 文件
// 源码路径: libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = pathList.findClass(name, suppressedExceptions);
}
return c;
}
}

DexPathList 内部的查找过程:

DexPathList.findClass(name)
→ 遍历 dexElements[] 数组
→ 对每个 Element: dexFile.loadClassBinaryName(name, definingContext, suppressed)
→ 找到第一个匹配的类就返回

这个遍历机制是 Android 热修复和插件化的核心:将补丁 DEX 文件插入到 dexElements 数组的前面,即可让补丁中的类优先被加载。

四、热修复的类加载原理

4.1 Tinker 方案(全量替换)

Tinker(腾讯开源)通过对比基准 APK 和新 APK,生成 DEX 差分包(.dex patch),然后在运行时合成新的完整 DEX,整体替换旧的 DEX:

1. 下载补丁: patch.dex
2. 与基准 DEX 合成: base.dex + patch.dex → new.dex
3. 将 new.dex 插入 PathClassLoader 的 dexElements 最前面
4. 下次加载类时,ClassLoader 先从 new.dex 查找

4.2 Sophix 方案(分派热更新)

Sophix(阿里)结合了多种方案:

  • 代码修复:底层替换 ArtMethod 结构体内的函数入口(inline hook 级别)
  • 资源修复:构造新的 AssetManager,替换 Resource 中的 mAssets 字段
  • So 修复:替换 ClassLoader 中的 nativeLibraryDirectories

4.3 核心实现:反射替换 dexElements

public static void installPatch(Context context, String patchDexPath) {
// 1. 获取应用的 PathClassLoader
PathClassLoader appClassLoader =
(PathClassLoader) context.getClassLoader();

// 2. 创建 DexClassLoader 加载补丁 DEX
DexClassLoader patchClassLoader = new DexClassLoader(
patchDexPath,
context.getCacheDir().getAbsolutePath(),
null,
appClassLoader
);

// 3. 反射获取 dexElements
Object appDexElements = getDexElements(appClassLoader);
Object patchDexElements = getDexElements(patchClassLoader);

// 4. 合并数组(补丁在前,原 DEX 在后)
Object mergedElements = mergeArray(patchDexElements, appDexElements);

// 5. 反射替换 dexElements
setDexElements(appClassLoader, mergedElements);
}

private static Object getDexElements(ClassLoader classLoader)
throws Exception {
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);

Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
return dexElementsField.get(pathList);
}

4.4 注意事项

  • Class验证问题:Android 5.0 之前使用 Dalvik 的 dvmVerifyClass 验证字节码,较严格。ART 取消了部分验证。补丁如果涉及类结构的重大变化(如新增字段、修改继承关系),可能触发 ClassDefNotFoundVerifyError
  • pre-verify 问题:Dalvik 中,如果类 A 引用了类 B,且 A 在 DEX1 中 B 在 DEX2 中,则需要 OPTIMIZED 标记。这在 MultiDex 场景下由 Gradle 插件自动处理。
  • Native 方法锚定:native 方法注册(RegisterNatives)发生在类加载后。热修复替换了类定义后,native 方法也需要重新注册。

五、Android N+ 的 ClassLoader 变化

5.1 API 24+ 的变化

Android 7.0 (API 24) 对 ClassLoader 做了重要调整:

  • 引入了 InMemoryDexClassLoader(API 26),允许直接从 ByteBuffer 加载 DEX 数据
  • DexClassLoaderoptimizedDirectory 参数在 API 26 被标记为 @Deprecated
  • ART 内部使用 DexFile 直接在内存中加载,不再需要解压到文件系统

5.2 Instant App 的 ClassLoader

Android Instant App 使用独立的 ClassLoader 层次:

BootClassLoader
→ PathClassLoader (Base APK)
→ Split ClassLoader (Feature modules)

每个 Feature Module 有自己的 Split ClassLoader,但类在模块间是共享的(通过设置 parent 关系)。

六、自定义 ClassLoader 实现

public class CustomClassLoader extends ClassLoader {
private String dexPath;

public CustomClassLoader(String dexPath, ClassLoader parent) {
super(parent);
this.dexPath = dexPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 检查是否已加载
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}

// 2. 从自定义路径加载
byte[] classData = loadClassData(name);
if (classData != null) {
return defineClass(name, classData, 0, classData.length);
}

// 3. 委托给父加载器
return super.findClass(name);
}

private byte[] loadClassData(String className) {
// 将类名转换为文件路径: com.example.MyClass → com/example/MyClass.class
String path = dexPath + "/" +
className.replace('.', '/') + ".class";
try {
FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
fis.close();
return bos.toByteArray();
} catch (IOException e) {
return null;
}
}
}

注意事项:

  • defineClassClassLoader 中真正将字节流转换为 JVM 中 Class 对象的方法。
  • 同一个 ClassLoader 不能对同一个类名调用两次 defineClass(会抛 LinkageError)。
  • 如果自定义 ClassLoader 需要打破双亲委派(如 Tomcat 的 WebAppClassLoader),应重写 loadClass 而非 findClass

七、Android R+ 的 InMemoryDexClassLoader 深入分析

7.1 为什么需要内存加载?

从 Android 8.0 (API 26) 开始引入 InMemoryDexClassLoader,到 Android 11 (API 30) 后逐渐成为热修复和插件化的推荐方案。其核心优势:

  • 零文件系统开销:DEX 数据直接从 ByteBuffer 加载,无需写入磁盘缓存目录
  • 安全性提升:不留下磁盘上的 DEX 文件痕迹,减少逆向风险
  • 性能提升:避免磁盘 I/O,加载速度更快
// API 26+: 从内存 ByteBuffer 直接加载 DEX
ByteBuffer dexBuffer = ByteBuffer.wrap(dexBytes);
InMemoryDexClassLoader cl = new InMemoryDexClassLoader(
dexBuffer,
getClassLoader() // parent
);

// 也可以加载多个 DEX
ByteBuffer[] dexBuffers = new ByteBuffer[] {
ByteBuffer.wrap(primaryDexBytes),
ByteBuffer.wrap(secondaryDexBytes)
};
InMemoryDexClassLoader cl = new InMemoryDexClassLoader(
dexBuffers,
getClassLoader()
);

7.2 与 PathClassLoader/DexClassLoader 的区别

在 API 26+ 上,三者的底层实现趋同——都通过 DexPathList 管理 DEX 文件,但初始化方式不同:

特性 InMemoryDexClassLoader DexClassLoader PathClassLoader
数据来源 ByteBuffer(内存) 文件路径 文件路径(系统路径)
optimizedDirectory 不需要 已废弃(API 26+) 不需要
热修复适用 最佳(无需文件权限) 可用 不推荐
API 要求 26+ 所有版本 所有版本

7.3 InMemoryDexClassLoader 在热修复中的应用

相比传统 DexClassLoader 方案需要将补丁 DEX 先写入磁盘(需要存储权限),InMemoryDexClassLoader 可以直接从网络下载的字节数组构造 ClassLoader:

// 现代热修复方案:下载补丁 → 内存加载 → 反射合并
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(patchUrl).build();
Response response = client.newCall(request).execute();
byte[] patchDexBytes = response.body().bytes();

// 直接在内存中加载,无需写磁盘
ByteBuffer buffer = ByteBuffer.wrap(patchDexBytes);
InMemoryDexClassLoader patchCL = new InMemoryDexClassLoader(buffer, appCL);

// 反射合并 dexElements(与之前方案相同)
Object mergedElements = mergeArray(
getDexElements(patchCL),
getDexElements(appCL)
);
setDexElements(appCL, mergedElements);

这消除了热修复方案对外部存储权限的依赖,也避免了补丁文件被篡改的风险。

八、类加载死锁场景与解决方案

8.1 经典死锁场景深入分析

在 2.3 节我们提到了类加载死锁的基本场景。这里深入分析实际 Android 开发中可能遇到的三种死锁模式:

模式一:循环依赖死锁

ClassLoader A 加载类 X → X 的静态初始化中引用类 Y
ClassLoader B 加载类 Y → Y 的静态初始化中引用类 X
线程 1: A.loadClass(X) → 持 X 锁 → 触发 B.loadClass(Y) → 等 Y 锁
线程 2: B.loadClass(Y) → 持 Y 锁 → 触发 A.loadClass(X) → 等 X 锁

模式二:跨 ClassLoader 的静态初始化死锁

// 在 Application 启动时最容易触发
// Thread 1: 加载主 dex 中的类
Class.forName("com.example.MainActivity"); // 需要初始化

// Thread 2: 同时加载依赖库中的类(如 MultiDex 安装期间)
// 如果两个类在 <clinit> 中互相引用,形成死锁

模式三:插件化中的 ClassLoader 父子反生死锁

当插件 ClassLoader 的 parent 被错误设置,导致插件加载宿主类、宿主又尝试加载插件类时,两者在不同 ClassLoader 的锁上互相等待。

8.2 解决方案

方案 1:延迟类解析(Android 默认)
loadClass(name, false)resolve=false 使得类加载和类解析分离。类加载只定义类结构,不解析其依赖;解析推迟到第一次使用时。这减少了锁持有时间。

方案 2:并行类加载的锁细化(Java 7+)
getClassLoadingLock(name) 返回的是每个类名独立的锁对象(而非 ClassLoader 级别的全局锁),不同类的加载可以并行进行。

// ClassLoader 源码中的锁细化
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
lock = new Object();
Object old = parallelLockMap.putIfAbsent(className, lock);
if (old != null) lock = old;
}
return lock;
}

方案 3:避免静态初始化块中的跨 ClassLoader 引用
static {} 块中避免加载其他 ClassLoader 管理的类。延迟初始化通过懒加载单例模式实现。

方案 4:统一 ClassLoader 管理
在插件化架构中,确保公共依赖(如基础库)由同一个 ClassLoader 加载,避免同一个类被多个 ClassLoader 分别加载导致类型不一致和死锁。

九、MultiDex 与类加载

当 APK 的方法数超过 65536 时,需要使用 MultiDex:

// build.gradle
android {
defaultConfig {
multiDexEnabled true
}
}
dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
}

MultiDex 的工作原理:

  1. Gradle 将所有 DEX 文件分为 classes.dex(主 DEX,包含启动必需的类)和 classes2.dexclasses3.dex 等。
  2. MultiDex.install(context) 在 Application.attachBaseContext() 中调用。
  3. 安装过程:解压 APK,将 classes2.dex 等复制到 data 目录,通过反射将路径注入 dexElements 数组。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this); // 必须在 super.attachBaseContext 之后
}
}

八、面试常问题目

Q1: 什么是双亲委派模型?为什么要这样设计?Android 的 ClassLoader 和 JVM 的有何不同?

双亲委派模型要求除了顶层的 Bootstrap ClassLoader 外,所有 ClassLoader 在加载一个类时优先委派给父 ClassLoader。设计目的:(1) 安全性——防止核心类库被篡改;(2) 唯一性——保证 Java 核心库的类型安全,避免同一个类被不同 ClassLoader 加载后引发 ClassCastException。Android 的差异:使用 BootClassLoader → PathClassLoader/DexClassLoader,没有 Extension ClassLoader 这一层。Android 的 BaseDexClassLoader 通过 DexPathList 管理多个 DEX 文件(MultiDex 场景),不遵循严格的双亲委派,在 findClass 中遍历多个 DEX。

Q2: PathClassLoader 和 DexClassLoader 的区别?在什么场景下分别使用?

在 API 26 之前:PathClassLoader 用于加载已安装 APK 中的 DEX(系统自动创建),DexClassLoader 需要指定 optimizedDirectory 用于加载外部路径的 DEX。PathClassLoader 是应用默认的 ClassLoader,DexClassLoader 用于插件化、热修复等加载外部 DEX 的场景。API 26+ 后两者几乎无区别,optimizedDirectory 被废弃,ART 直接在内存中加载 DEX。

Q3: Android 热修复的类加载原理是什么?为什么补丁 DEX 要放在 dexElements 最前面?

热修复的核心是通过反射修改 PathClassLoader 中 DexPathList 的 dexElements 数组,将补丁 DEX 元素插入到数组的最前面。因为 ClassLoader 在 findClass 时从前到后遍历 dexElements,找到匹配的类就立即返回——所以放在前面的补丁 DEX 中的类会被优先加载,从而”覆盖”了原始 DEX 中的同名类。这个机制与双亲委派无关,是在 findClass 阶段通过搜索顺序实现的。

Q4: 为什么会出现 ClassCastException: Foo cannot be cast to Foo

这种异常出现在同一个类的不同 ClassLoader 加载的场景。在 JVM/ART 中,类的完整标识是 (ClassLoader, package.ClassName)——即使全限定名完全相同,由不同 ClassLoader 加载的类在 JVM 看来是不同的类型。常见场景:(1) 双亲委派被打破后,Parent ClassLoader 和 Child ClassLoader 分别加载了同一个类;(2) 插件化/热修复中,插件和宿主各自打包了相同的第三方库。解决方案是确保公共依赖库只被一个 ClassLoader 加载。

Q5: 类加载过程中有哪些锁?可能导致什么并发问题?

ClassLoader.loadClass 内部通过 getClassLoadingLock(name) 获取同步锁,确保同一个类不会被并发加载多次。主要的锁是类级别的——同一全限定名只允许一个线程执行 defineClass。死锁场景:两个 ClassLoader 在加载各自类时互相依赖对方加载的类,形成循环等待。Android 通过在 loadClass 中使用 resolve=false 延迟类解析,以及限制 native 层的类链接锁粒度来缓解此问题。


参考源码路径:

  • Android ClassLoader:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
  • PathClassLoader:libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
  • DexClassLoader:libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
  • InMemoryDexClassLoader:libcore/dalvik/src/main/java/dalvik/system/InMemoryDexClassLoader.java
  • ART 类链接:art/runtime/class_linker.cc
  • MultiDex:androidx/multidex/src/main/java/androidx/multidex/MultiDex.java
打赏
  • 微信
  • 支付宝

评论