虚拟机与类加载
# 13.虚拟机与类加载
# 目录介绍
- 01.为什么Android需要虚拟机
- 1.1 虚拟机的必要性
- 1.2 Android虚拟机的特殊性
- 02.Dalvik与ART的演进
- 2.1 Dalvik时代(Android 1.0 ~ 4.4)
- 2.2 ART时代(Android 5.0+)
- 2.3 Dalvik vs ART对比
- 03.DEX字节码与Java字节码的区别
- 3.1 指令集差异
- 3.2 DEX指令格式
- 3.3 文件格式差异
- 04.ART虚拟机架构详解
- 4.1 ART的核心组件
- 4.2 执行引擎的三种模式
- 4.3 方法执行入口选择
- 05.JIT与AOT编译原理
- 5.1 JIT编译器工作原理
- 5.2 AOT编译与dex2oat
- 5.3 编译产物的关系
- 06.Profile-Guided编译
- 6.1 Profile收集
- 6.2 后台dex优化
- 6.3 Cloud Profiles(Android 12+)
- 07.类加载器体系
- 7.1 Android类加载器层级
- 7.2 ClassLoader源码
- 08.类加载的双亲委派模型
- 8.1 双亲委派流程
- 8.2 为什么需要双亲委派
- 8.3 打破双亲委派
- 09.PathClassLoader与DexClassLoader
- 9.1 DexPathList核心结构
- 9.2 DexFile的加载
- 10.MultiDex的原理与实现
- 10.1 为什么需要MultiDex
- 10.2 MultiDex在ART中的实现
- 10.3 MultiDex Support Library(Dalvik时代)
- 11.类校验与优化
- 11.1 类验证的必要性
- 11.2 类初始化
- 12.对象内存布局与分配
- 12.1 对象的内存布局
- 12.2 对象分配策略
- 13.方法调用与虚方法分派
- 13.1 方法调用类型
- 13.2 虚方法表(vtable)
- 13.3 接口方法表(itable)
- 14.ART中的内联缓存
- 14.1 什么是内联缓存
- 14.2 JIT中的内联缓存
- 15.类加载在热修复中的应用
- 15.1 基于dexElements的类替换
- 15.2 CLASS_ISPREVERIFIED问题
- 15.3 Instant Run与热修复对比
- 16.总结与技术思考
- 16.1 核心要点回顾
- 16.2 面试高频问题
- 16.3 学习建议
# 01.为什么Android需要虚拟机
# 1.1 虚拟机的必要性
疑惑:为什么Android不直接运行原生C/C++代码,而要通过虚拟机?
答疑:有三个核心原因:
第一,跨平台兼容性。Android设备有ARM、x86、MIPS等不同的CPU架构。如果开发者直接编写特定架构的机器码,一个应用需要为每种架构单独编译。虚拟机提供了一个抽象层,开发者只需编写一次字节码,虚拟机负责在不同平台上执行。
第二,安全性。虚拟机可以对字节码进行验证,确保代码不会执行非法操作(如数组越界、类型不安全的转换)。虚拟机还提供了内存管理和垃圾回收,防止内存泄漏和悬垂指针等问题。
第三,可管理性。虚拟机可以监控应用的资源使用,在必要时进行限制和清理。这对移动设备的资源管理至关重要。
# 1.2 Android虚拟机的特殊性
Android没有使用标准的JVM,而是设计了专用的虚拟机,原因:
Android虚拟机 vs 标准JVM:
内存限制:
JVM:运行在服务器/PC上,内存充裕(GB级别)
Android:运行在移动设备上,内存受限(早期设备仅256MB)
电池限制:
JVM:不需要考虑功耗
Android:需要极度省电,CPU频率和使用时间都受限
启动速度:
JVM:长期运行的服务,启动速度不敏感
Android:用户期望应用秒开,启动速度至关重要
存储限制:
JVM:存储空间充裕
Android:早期设备内部存储仅1-2GB
结果:Android设计了Dalvik/ART虚拟机
└── 基于寄存器而非栈的指令集(更少指令数)
└── DEX格式(共享常量池,比JAR更紧凑)
└── 针对低内存优化的GC策略
└── Zygote fork预热启动
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 02.Dalvik与ART的演进
# 2.1 Dalvik时代(Android 1.0 ~ 4.4)
Dalvik虚拟机特点:
架构:基于寄存器的虚拟机
└── 相比JVM的栈式架构,指令更少但更复杂
└── 一条Dalvik指令 ≈ 2-3条JVM指令
执行方式:解释执行 + JIT
├── Android 2.2之前:纯解释执行
│ └── 逐条解释字节码,速度慢
└── Android 2.2+:加入JIT编译器
└── 运行时将热点方法编译为机器码
└── 编译结果存在内存中,进程退出即丢失
进程模型:
└── 每个应用运行在独立的Dalvik VM实例中
└── 通过Zygote fork创建,共享只读内存(类库等)
缺点:
├── JIT每次启动都要重新编译
├── GC效率低(Stop-The-World时间长)
└── 解释执行性能差
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2.2 ART时代(Android 5.0+)
ART虚拟机的重大改进:
Android 5.0~6.0(ART初版):
├── 安装时AOT编译(dex2oat将全部DEX编译为机器码)
├── 新的GC算法(并发标记清除)
├── 更好的内存管理
└── 缺点:安装时间长,占用存储大
Android 7.0+(ART改进版):
├── 混合编译:解释执行 + JIT + AOT
├── Profile-guided compilation
├── Concurrent Copying GC(更低的暂停时间)
└── 安装时不编译,空闲时根据Profile编译
Android 10+(ART持续优化):
├── Generational GC(分代垃圾回收)
├── Bionic Allocator优化
└── 更好的启动优化
Android 12+:
├── Cloud Profiles
├── ART模块化(Mainline更新)
└── 可通过Google Play更新ART运行时
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.3 Dalvik vs ART对比
Dalvik与ART关键对比:
Dalvik ART
字节码格式 DEX DEX
指令集 基于寄存器 基于寄存器
编译方式 解释+JIT 解释+JIT+AOT
安装速度 快 快(7.0+) / 慢(5.0)
启动速度 慢(每次JIT) 快(有AOT代码)
运行性能 较差 好(接近原生)
存储占用 小 大(有编译产物)
GC暂停 10-50ms <1ms(CC GC)
内存使用 较高 较低(Compact GC)
2
3
4
5
6
7
8
9
10
11
12
# 03.DEX字节码与Java字节码的区别
# 3.1 指令集差异
Java字节码(基于栈)vs DEX字节码(基于寄存器):
计算 a = b + c 的字节码对比:
Java字节码(JVM,基于栈):
iload_1 // 将b压入操作数栈
iload_2 // 将c压入操作数栈
iadd // 弹出栈顶两个值相加,结果压回栈
istore_0 // 弹出栈顶值存入局部变量a
共4条指令
DEX字节码(Dalvik/ART,基于寄存器):
add-int v0, v1, v2 // 将v1+v2的结果存入v0
共1条指令
寄存器架构的优势:
更少的指令数 → 更少的指令分发开销
直接操作寄存器 → 减少内存访问
缺点:指令更长(需要编码寄存器号)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.2 DEX指令格式
DEX指令的编码格式:
格式 大小 描述
─────────────────────────────────
10x 1 unit 操作码,无操作数
12x 1 unit 操作码 + 两个4bit寄存器
11n 1 unit 操作码 + 寄存器 + 4bit立即数
22x 2 unit 操作码 + 8bit寄存器 + 16bit寄存器
23x 2 unit 操作码 + 3个8bit寄存器
31i 3 unit 操作码 + 8bit寄存器 + 32bit立即数
35c 3 unit 操作码 + 方法调用(最多5个参数)
示例:invoke-virtual指令
35c格式: B|A|op CCCC G|F|E|D
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(I)V
op = 0x6e (invoke-virtual)
参数寄存器: v0 (this), v1 (参数)
方法引用: CCCC = method_id索引
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.3 文件格式差异
.class文件 vs .dex文件:
JAR包中的.class文件:
├── ClassA.class (有自己的常量池)
├── ClassB.class (有自己的常量池)
└── ClassC.class (有自己的常量池)
→ 如果三个类都用了String "hello",该字符串存储3次
DEX文件:
├── 共享字符串常量池 (所有类共用)
├── 共享类型表
├── 共享方法表
├── ClassA的代码
├── ClassB的代码
└── ClassC的代码
→ "hello"只存储1次,三个类引用同一个索引
DEX优势:
文件更小(消除冗余)
加载更快(一次性映射到内存)
更适合移动设备的存储和内存限制
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 04.ART虚拟机架构详解
# 4.1 ART的核心组件
ART虚拟机架构:
┌──────────────────────────────────────────┐
│ ART Runtime │
│ ┌────────────────────────────────────┐ │
│ │ Execution Engine │ │
│ │ ├── Interpreter (解释器) │ │
│ │ ├── JIT Compiler (即时编译器) │ │
│ │ └── AOT Code (预编译代码) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ Garbage Collector │ │
│ │ ├── Concurrent Copying GC (CC) │ │
│ │ ├── Generational Mode │ │
│ │ └── Compacting (内存压缩) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ Class Linker │ │
│ │ ├── 类加载 │ │
│ │ ├── 类验证 │ │
│ │ ├── 类解析 │ │
│ │ └── 类初始化 │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ Memory Management │ │
│ │ ├── Heap (Java堆) │ │
│ │ ├── Rosalloc (小对象分配器) │ │
│ │ ├── DlMalloc (大对象分配器) │ │
│ │ └── TLAB (线程本地分配缓冲) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ Thread Management │ │
│ │ ├── Thread (ART线程) │ │
│ │ ├── Monitor (对象锁) │ │
│ │ └── SafePoint (安全点) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 4.2 执行引擎的三种模式
// ART执行引擎的三种执行方式
// 1.解释执行
// 逐条解读DEX字节码,最慢但最灵活
void ExecuteInterpretedMethod(ArtMethod* method, ...) {
const uint16_t* dex_pc = method->GetCodeItem()->insns_;
while (true) {
uint16_t inst = *dex_pc;
switch (inst & 0xFF) {
case Instruction::MOVE:
// 处理MOVE指令
break;
case Instruction::INVOKE_VIRTUAL:
// 处理虚方法调用
break;
// ... 处理所有指令
}
dex_pc += inst_size;
}
}
// 2.JIT编译执行
// 运行时将热点方法编译为机器码
// JIT编译后的代码存储在JIT Code Cache中
void JitCompile(ArtMethod* method) {
// 构建中间表示
HGraph* graph = BuildGraph(method);
// 优化
RunOptimizationPasses(graph);
// 生成机器码
EmitCode(graph, jit_code_cache);
}
// 3.AOT编译代码
// 安装时由dex2oat预编译,直接执行机器码
// 性能最好,与原生代码接近
void ExecuteCompiledCode(ArtMethod* method) {
// 直接跳转到编译后的机器码入口
method->GetEntryPointFromQuickCompiledCode()();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 4.3 方法执行入口选择
ART为每个方法维护入口指针,决定使用哪种执行方式:
方法入口选择逻辑:
ArtMethod
├── entry_point_from_quick_compiled_code_
│ ├── 如果有AOT编译代码 → 指向OAT文件中的机器码
│ ├── 如果有JIT编译代码 → 指向JIT Code Cache
│ └── 如果都没有 → 指向解释器桥接函数
│ └── art_quick_to_interpreter_bridge
│
└── entry_point_from_interpreter_
└── 指向解释器入口
热度计数器:
每个方法有一个hotness_count_
当计数达到阈值(默认10000)时:
└── JIT编译器在后台编译该方法
└── 编译完成后更新entry_point指向JIT代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 05.JIT与AOT编译原理
# 5.1 JIT编译器工作原理
JIT编译流程(Android 7.0+ ART):
1. 热度检测
└── 方法被调用或循环回边执行时计数+1
└── 达到阈值 → 加入JIT编译队列
2. OSR (On-Stack Replacement)
└── 对于长时间运行的循环
└── 无需等待方法退出,直接替换运行中的代码
3. 编译过程
├── 构建HGraph(高层中间表示)
│ └── 将DEX字节码转换为SSA形式的图
├── 优化Pass
│ ├── Inlining (方法内联)
│ ├── Constant Folding (常量折叠)
│ ├── Dead Code Elimination (死代码消除)
│ ├── Bounds Check Elimination (边界检查消除)
│ ├── Load Store Elimination (加载存储消除)
│ └── Register Allocation (寄存器分配)
└── 代码生成
└── 生成目标平台的机器码(ARM64/ARM/x86)
4. 代码安装
└── 将编译后的代码写入JIT Code Cache
└── 更新方法的入口指针
└── 下次调用直接执行编译后的代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 5.2 AOT编译与dex2oat
dex2oat编译管线:
输入:classes.dex
│
├── 前端:DEX → HGraph
│ └── 每个方法转换为HGraph(SSA中间表示)
│
├── 中端:优化
│ ├── InstructionSimplifier
│ ├── ConstantFolding
│ ├── InstructionCombiner
│ ├── GVN (Global Value Numbering)
│ ├── Inlining
│ ├── BoundsCheckElimination
│ ├── LoopOptimization
│ └── SideEffectsAnalysis
│
├── 后端:代码生成
│ ├── RegisterAllocator (线性扫描/图着色)
│ ├── InstructionScheduling
│ └── CodeGenerator (ARM64/ARM/x86)
│
└── 输出:
├── .oat文件 (ELF格式,包含编译后的机器码)
│ ├── oat_header (版本信息)
│ ├── .rodata (只读数据)
│ ├── .text (编译后的机器码)
│ └── .bss (GC根引用)
├── .vdex文件 (验证后的DEX)
│ └── 包含原始DEX + quickening信息
└── .art文件 (预初始化的堆)
└── 预创建的Class对象和String对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 5.3 编译产物的关系
编译产物文件关系:
base.apk (原始APK,包含classes.dex)
│
├── dex2oat编译
│
├── base.odex (.oat格式)
│ └── 编译后的机器码
│ └── 运行时ART加载执行
│
├── base.vdex
│ └── 验证后的DEX + 优化信息
│ └── 用途1:回退到解释执行时使用
│ └── 用途2:加速重编译
│
└── base.art (boot image的扩展)
└── 预初始化的ART堆数据
└── 包含Class对象、String常量等
└── 进程启动时直接映射到内存
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 06.Profile-Guided编译
# 6.1 Profile收集
Android 7.0+的混合编译核心是Profile-Guided Optimization:
Profile收集过程:
应用运行时:
├── JIT编译器记录热点方法
├── 运行时记录"热"类(使用频率高的类)
└── Profile信息写入文件
Profile文件位置:
/data/misc/profiles/cur/0/com.example.app/primary.prof
└── 当前运行的Profile
/data/misc/profiles/ref/com.example.app/primary.prof
└── 参考Profile(用于AOT编译)
Profile包含的信息:
├── 热点方法列表 (Hot Methods)
│ └── 调用次数超过阈值的方法
├── 启动方法列表 (Startup Methods)
│ └── 应用启动阶段调用的方法
├── 热类列表 (Hot Classes)
│ └── 使用频率高的类
└── 内联缓存数据 (Inline Caches)
└── 虚方法调用的实际接收者类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 6.2 后台dex优化
后台dex优化流程(BackgroundDexOptService):
1. 触发时机
├── 设备充电 + 空闲
└── 通过JobScheduler调度
2. 检查是否需要优化
├── 是否有新的Profile数据
├── Profile是否足够大(有足够的热点信息)
└── 距离上次优化是否超过一定时间
3. 执行编译
└── dex2oat --compiler-filter=speed-profile
├── 只编译Profile中的热点方法
├── 其他方法保持DEX形式
└── 编译结果替换旧的OAT文件
4. 效果
├── 热点方法:AOT编译,运行最快
├── 非热点方法:解释执行或JIT
└── 编译产物大小 ≈ 全量编译的20-30%
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 6.3 Cloud Profiles(Android 12+)
Cloud Profiles工作流程:
1. Google Play聚合Profile
├── 收集大量用户设备上的Profile数据
├── 聚合得到"通用"的热点方法集
└── 上传到Play Store服务器
2. 安装时下载Profile
└── 用户安装应用时从Play Store下载Cloud Profile
3. 安装时编译
└── 使用Cloud Profile进行AOT编译
└── 首次运行就有近乎完全编译的性能
优势:
└── 解决了"首次运行冷启动慢"的问题
└── 不需要等待本地Profile积累
└── 覆盖了大多数用户的热点代码路径
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 07.类加载器体系
# 7.1 Android类加载器层级
Android类加载器层级:
BootClassLoader (引导类加载器)
├── 加载Android框架核心类
├── java.lang.*, java.util.*, android.* 等
└── 对应boot image(/system/framework/boot-xxx.art)
│
├── PathClassLoader (路径类加载器)
│ ├── 加载已安装的APK中的类
│ ├── 系统在创建应用进程时自动创建
│ ├── dexPath = APK路径
│ └── librarySearchPath = Native库路径
│
├── DexClassLoader (DEX类加载器)
│ ├── 可以加载任意路径的DEX/JAR/APK
│ ├── 插件化框架常用
│ └── 与PathClassLoader的区别在Android 8.0+已消失
│
└── InMemoryDexClassLoader (内存DEX加载器)
├── Android 8.0+新增
└── 从内存中的ByteBuffer加载DEX
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7.2 ClassLoader源码
// BaseDexClassLoader.java - PathClassLoader和DexClassLoader的父类
public class BaseDexClassLoader extends ClassLoader {
// 核心:DexPathList管理所有DEX文件
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath,
librarySearchPath, null, false);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在DexPathList中查找类
Class<?> c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
throw new ClassNotFoundException(name);
}
return c;
}
}
// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
// Android 8.0+: optimizedDirectory参数被忽略
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
}
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
// Android 8.0+: optimizedDirectory参数被忽略
// 实际上和PathClassLoader已经没有区别
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 08.类加载的双亲委派模型
# 8.1 双亲委派流程
// ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1.检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2.委派给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有父加载器,使用BootClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载,不做处理
}
// 3.父加载器无法加载,自己尝试加载
if (c == null) {
c = findClass(name);
}
}
return c;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
双亲委派的调用链:
加载 com.example.MyActivity:
PathClassLoader.loadClass("com.example.MyActivity")
│
├── 1.findLoadedClass() → null (未加载过)
│
├── 2.parent.loadClass() → BootClassLoader
│ ├── findLoadedClass() → null
│ └── findClass() → ClassNotFoundException
│ (BootClassLoader中没有com.example开头的类)
│
└── 3.findClass("com.example.MyActivity")
└── pathList.findClass()
└── 遍历dexElements[]
└── 在APK的DEX中找到该类
└── defineClass() → 返回Class对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 8.2 为什么需要双亲委派
双亲委派的好处:
1. 安全性
└── 防止用户自定义java.lang.String等核心类
└── 核心类始终由BootClassLoader加载
└── 即使APK中包含同名类也不会被加载
2. 避免重复加载
└── 父加载器已加载的类不会被子加载器重复加载
└── 减少内存使用
3. 类的唯一性
└── 同一个类被同一个ClassLoader加载才是同一个类
└── 不同ClassLoader加载的同名类被认为是不同类
└── 这是插件化框架中需要注意的重要问题
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 8.3 打破双亲委派
在某些场景下需要打破双亲委派:
// 自定义ClassLoader打破双亲委派
class PluginClassLoader extends DexClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 对于插件自己的类,不委派给父加载器
// 直接在自己的DEX中查找
// 1.已加载的类直接返回
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// 2.系统类仍然走双亲委派
if (name.startsWith("java.") || name.startsWith("android.")) {
return getParent().loadClass(name);
}
// 3.插件类优先从自己的DEX加载(打破双亲委派)
try {
c = findClass(name);
if (c != null) return c;
} catch (ClassNotFoundException e) {
// 找不到,回退到父加载器
}
// 4.回退到双亲委派
return getParent().loadClass(name);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 09.PathClassLoader与DexClassLoader
# 9.1 DexPathList核心结构
两者的核心都是DexPathList中的dexElements数组:
// DexPathList.java
class DexPathList {
// 核心数据结构:Element数组
private Element[] dexElements;
// 每个Element对应一个DEX文件
static class Element {
private final File path; // 文件路径
private final DexFile dexFile; // DEX文件对象
public Class<?> findClass(String name) {
return dexFile != null ? dexFile.loadClassBinaryName(name,
definingContext, suppressedExceptions) : null;
}
}
// 查找类:遍历dexElements数组
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
// 遍历所有Element都没找到
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
dexElements数组的查找顺序:
dexElements[0] → classes.dex (优先查找)
dexElements[1] → classes2.dex
dexElements[2] → classes3.dex
...
dexElements[N] → classesN+1.dex
关键点:类在第一个匹配的DEX中被找到后就停止搜索。
这个特性是MultiDex和热修复的基础。
2
3
4
5
6
7
8
9
10
# 9.2 DexFile的加载
// DexFile.java
public final class DexFile {
// 打开DEX文件(Native实现)
private static Object openDexFile(String sourceName, String outputName,
int flags, ClassLoader loader, DexPathList.Element[] elements) {
// 通过JNI调用到ART Runtime
return openDexFileNative(sourceName, outputName, flags, loader, elements);
}
// Native层的实现
// art/runtime/native/dalvik_system_DexFile.cc
static jobject DexFile_openDexFileNative(JNIEnv* env, ...) {
// 1.打开文件
std::unique_ptr<const DexFile> dex_file = DexFileLoader::Open(
sourceName, ...);
// 2.如果有OAT编译产物,加载OAT
OatFile* oat_file = OatFileManager::OpenOatFile(sourceName);
// 3.验证DEX
if (!dex_file->IsVerified()) {
VerifyDexFile(dex_file);
}
return CreateCookieFromDexFiles(env, dex_files, oat_file);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 10.MultiDex的原理与实现
# 10.1 为什么需要MultiDex
疑惑:为什么一个DEX文件不够用?
答疑:DEX文件中方法引用使用16位索引,最多引用65536个方法。当应用引入大量第三方库后很容易超过这个限制:
65536方法数限制:
DEX文件的method_ids区域使用uint16_t索引
最大值 = 2^16 = 65536
一个中型应用的方法数分布:
├── Android SDK: ~15000个方法
├── AppCompat: ~12000个方法
├── Google Services: ~20000个方法
├── 其他第三方库: ~15000个方法
└── 应用自身代码: ~8000个方法
合计: ~70000 > 65536 → 溢出!
解决方案:
Android 5.0+ (ART): 原生支持MultiDex
Android 4.4- (Dalvik): 需要MultiDex Support Library
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 10.2 MultiDex在ART中的实现
ART原生MultiDex支持:
APK中包含多个DEX文件:
├── classes.dex (主DEX)
├── classes2.dex (第二个DEX)
├── classes3.dex (第三个DEX)
└── ...
ART安装时处理:
dex2oat会将所有DEX文件合并编译:
classes.dex + classes2.dex + classes3.dex
→ 统一编译为一个OAT文件
→ base.odex
运行时加载:
PathClassLoader的DexPathList中
dexElements[] 包含所有DEX文件的Element
查找类时顺序遍历所有Element
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 10.3 MultiDex Support Library(Dalvik时代)
// MultiDex.java(Support Library)
public class MultiDex {
public static void install(Context context) {
// 1.获取应用的ClassLoader
ClassLoader loader = context.getClassLoader();
// 2.获取APK文件路径
ApplicationInfo appInfo = context.getApplicationInfo();
File sourceApk = new File(appInfo.sourceDir);
// 3.提取secondary DEX文件
File dexDir = getDexDir(context, appInfo);
List<File> files = MultiDexExtractor.load(context, appInfo, dexDir);
// 4.关键:将secondary DEX注入到dexElements数组中
installSecondaryDexes(loader, dexDir, files);
}
private static void installSecondaryDexes(ClassLoader loader,
File dexDir, List<File> files) {
// 反射获取PathClassLoader的pathList字段
Field pathListField = findField(loader, "pathList");
Object pathList = pathListField.get(loader);
// 反射获取dexElements字段
Field dexElementsField = findField(pathList, "dexElements");
Object[] originElements = (Object[]) dexElementsField.get(pathList);
// 为secondary DEX创建新的Element
Object[] newElements = makeDexElements(files, ...);
// 合并:原有Elements + 新Elements
Object[] combined = new Object[originElements.length + newElements.length];
System.arraycopy(originElements, 0, combined, 0, originElements.length);
System.arraycopy(newElements, 0, combined, originElements.length, newElements.length);
// 设置回去
dexElementsField.set(pathList, combined);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 11.类校验与优化
# 11.1 类验证的必要性
ART在加载类时会进行严格的验证,确保字节码的合法性:
类验证检查的内容:
1. 结构验证
├── DEX文件格式正确性
├── 字符串编码合法性
└── 索引范围合法性
2. 语义验证
├── 类继承关系合法
├── 方法重写签名正确
└── 接口实现完整
3. 字节码验证
├── 指令编码合法
├── 寄存器使用不越界
├── 类型安全(不会把int当作Object用)
├── 控制流合法(不会跳转到指令中间)
└── 异常处理范围合法
验证结果:
├── 验证通过 → 标记为verified,可以优化
├── 验证失败 → 标记为errored,不能加载
└── 软失败 → 运行时再次验证
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 11.2 类初始化
// 类初始化的触发时机(与JVM规范一致)
// 以下操作触发类初始化:
// 1. new 创建类的实例
// 2. 访问类的静态字段(非final常量)
// 3. 调用类的静态方法
// 4. 反射访问
// 5. 子类初始化时先初始化父类
// ART中的类状态
enum ClassStatus {
kNotReady, // 未准备
kIdx, // 索引已解析
kLoaded, // 已加载
kResolving, // 正在解析
kResolved, // 已解析(字段和方法已链接)
kVerifying, // 正在验证
kVerified, // 已验证
kInitializing, // 正在初始化(执行<clinit>)
kInitialized, // 已初始化(可以使用)
kVisiblyInitialized // 初始化对所有线程可见
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 12.对象内存布局与分配
# 12.1 对象的内存布局
ART中Java对象的内存布局:
普通对象:
┌───────────────────────┐
│ Object Header (8字节) │
│ ├── klass_ (4字节) │ → 指向Class对象
│ └── monitor_ (4字节) │ → 锁状态/HashCode/GC标记
├───────────────────────┤
│ 字段1 (按对齐排列) │
│ 字段2 │
│ ... │
│ 字段N │
└───────────────────────┘
数组对象:
┌───────────────────────┐
│ Object Header (8字节) │
├───────────────────────┤
│ length (4字节) │ → 数组长度
├───────────────────────┤
│ element[0] │
│ element[1] │
│ ... │
│ element[N-1] │
└───────────────────────┘
String对象(Android 9+ Compact Strings):
┌───────────────────────┐
│ Object Header (8字节) │
├───────────────────────┤
│ count (4字节) │ → 字符数
│ hash (4字节) │ → 缓存的HashCode
├───────────────────────┤
│ 字符数据 │ → 直接存储(不再是char[]引用)
└───────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 12.2 对象分配策略
ART的对象分配策略:
小对象 (< 128KB):
├── TLAB分配 (Thread Local Allocation Buffer)
│ └── 每个线程有私有的分配缓冲区
│ └── 无锁分配,速度极快(几条指令)
│ └── TLAB用完后从RegionSpace分配新的
│
├── Rosalloc分配器(旧版)
│ └── 按大小分桶(16B/24B/32B/...)
│ └── 每个桶有线程本地缓存
│ └── 类似TCMalloc的设计
│
└── BumpPointer分配
└── 简单地移动分配指针
└── 在CC GC的to-space中使用
大对象 (>= 128KB):
└── Large Object Space
└── 直接mmap分配
└── 独立管理,不参与Compact
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 伪代码:TLAB分配流程
Object allocate(int size) {
// 1.尝试TLAB分配(最快路径)
Thread* self = Thread::Current();
byte* top = self->tlabTop;
byte* end = self->tlabEnd;
if (top + size <= end) {
self->tlabTop = top + size;
return (Object*)top; // 成功,几条指令完成
}
// 2.TLAB空间不足,申请新的TLAB
if (AllocateNewTLAB(self, size)) {
return AllocateInTLAB(self, size);
}
// 3.无法分配新TLAB,触发GC
CollectGarbage();
return RetryAllocate(size);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 13.方法调用与虚方法分派
# 13.1 方法调用类型
DEX中的五种方法调用指令:
invoke-virtual:虚方法调用(最常见)
└── 需要运行时根据对象实际类型查找方法
└── 通过vtable(虚方法表)分派
invoke-interface:接口方法调用
└── 比虚方法调用更慢
└── 通过itable(接口方法表)分派
invoke-direct:直接调用
└── 构造方法、private方法
└── 编译时就确定目标,无需查表
invoke-static:静态方法调用
└── 静态方法,无需this引用
└── 编译时确定目标
invoke-super:调用父类方法
└── super.xxx()
└── 编译时确定为父类的vtable条目
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 13.2 虚方法表(vtable)
vtable的结构和查找过程:
class Animal {
void eat() { ... } // vtable[0]
void sleep() { ... } // vtable[1]
}
class Dog extends Animal {
void eat() { ... } // 重写,vtable[0]指向Dog.eat
void bark() { ... } // 新方法,vtable[2]
}
Dog对象的vtable:
┌──────────────────┐
│ [0] Dog.eat() │ ← 重写了Animal.eat
│ [1] Animal.sleep │ ← 继承,未重写
│ [2] Dog.bark() │ ← 新增方法
└──────────────────┘
虚方法调用过程:
animal.eat() // animal实际类型是Dog
1. 获取对象的Class指针
2. 从Class中获取vtable
3. 用方法的vtable索引(0)查找
4. 调用vtable[0] → Dog.eat()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 13.3 接口方法表(itable)
itable的查找比vtable多一次遍历:
interface Runnable { void run(); }
interface Comparable { int compareTo(Object o); }
class MyClass implements Runnable, Comparable {
void run() { ... }
int compareTo(Object o) { ... }
}
MyClass的itable:
┌────────────────────────────────────┐
│ Interface │ Offset │ Methods │
├────────────────────────────────────┤
│ Runnable │ 0 │ run() │
│ Comparable │ 1 │ compareTo() │
└────────────────────────────────────┘
接口方法调用过程:
Runnable r = new MyClass();
r.run();
1. 获取对象的Class
2. 在itable中查找Runnable接口
3. 获取run()方法的偏移量
4. 调用对应的实现方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 14.ART中的内联缓存
# 14.1 什么是内联缓存
虚方法调用每次都要查vtable/itable,开销不小。内联缓存是一种优化,记录上次调用的实际类型:
内联缓存原理:
未优化的虚方法调用:
animal.eat()
→ 每次查vtable → 调用方法
单态内联缓存(Monomorphic):
记录:上次调用时animal的类型是Dog
if (animal.class == Dog) {
直接调用Dog.eat() ← 命中缓存,无需查表
} else {
查vtable,更新缓存
}
多态内联缓存(Polymorphic):
记录最近N个不同类型
if (animal.class == Dog) → Dog.eat()
elif (animal.class == Cat) → Cat.eat()
elif (animal.class == Bird) → Bird.eat()
else → 查vtable
超多态(Megamorphic):
类型太多,退化为普通vtable查找
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 14.2 JIT中的内联缓存
JIT编译器利用内联缓存进行去虚拟化:
// 原始代码
void feedAnimal(Animal animal) {
animal.eat(); // 虚方法调用
}
// 如果Profile显示animal总是Dog类型
// JIT编译为:
void feedAnimal(Animal animal) {
if (animal instanceof Dog) { // 类型守卫
// 去虚拟化:直接调用Dog.eat()
// 可能进一步内联Dog.eat()的代码
((Dog)animal).inlinedEat();
} else {
// 慢路径:走vtable
animal.eat();
}
}
优化效果:
消除了vtable查找开销
方法内联后可以进一步优化(常量传播、死代码消除等)
对于单态调用点,性能提升显著
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 15.类加载在热修复中的应用
# 15.1 基于dexElements的类替换
热修复的核心思路是利用dexElements的顺序查找特性:
热修复原理(dexElements插入法):
修复前的dexElements:
[Element(classes.dex)] → 包含BugClass(有Bug)
修复操作:
将包含修复后BugClass的patch.dex插入到数组前面
修复后的dexElements:
[Element(patch.dex), Element(classes.dex)]
↑ 包含修复后的BugClass ↑ 包含有Bug的BugClass
类加载时:
loadClass("BugClass")
→ 遍历dexElements
→ 先在patch.dex中找到BugClass → 返回修复后的版本
→ classes.dex中的旧版本永远不会被加载
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 热修复:注入patch dex到dexElements最前面
void hotFix(Context context, File patchDex) {
// 1.获取应用的ClassLoader
PathClassLoader classLoader = (PathClassLoader) context.getClassLoader();
// 2.反射获取pathList
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
// 3.反射获取dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] originElements = (Object[]) dexElementsField.get(pathList);
// 4.为patch dex创建新的Element
Object[] patchElements = makeDexElements(patchDex);
// 5.关键:patch放在前面
Object[] combined = new Object[patchElements.length + originElements.length];
System.arraycopy(patchElements, 0, combined, 0, patchElements.length);
System.arraycopy(originElements, 0, combined, patchElements.length,
originElements.length);
// 6.设置回去
dexElementsField.set(pathList, combined);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 15.2 CLASS_ISPREVERIFIED问题
在Dalvik虚拟机上,热修复会遇到CLASS_ISPREVERIFIED问题:
CLASS_ISPREVERIFIED问题:
Dalvik在安装时会对DEX进行验证优化(dexopt):
如果一个类的所有引用都在同一个DEX文件中
→ 该类被标记为CLASS_ISPREVERIFIED
运行时:
如果被标记的类引用了其他DEX文件中的类
→ 抛出 IllegalAccessError
热修复的影响:
BugClass在classes.dex中被标记为ISPREVERIFIED
patch.dex中的修复类引用了classes.dex中的其他类
→ 报错!
解决方案(QQ空间方案):
在每个类的构造方法中插入一行代码:
System.out.println(AntiLazyLoad.class);
AntiLazyLoad.class放在独立的hack.dex中
→ 所有类都引用了不同DEX中的类
→ 不会被标记为ISPREVERIFIED
ART不存在此问题,因为ART没有CLASS_ISPREVERIFIED机制。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 15.3 Instant Run与热修复对比
热修复方案对比:
方案 原理 重启要求 粒度
─────────────────────────────────────────────────────────────
dexElements插入 在ClassLoader中注入新DEX 冷启动 类级别
Tinker 合成全量DEX替换 冷启动 类级别
AndFix Native层替换ArtMethod 不需要 方法级别
Robust 字节码插桩+方法路由 不需要 方法级别
Instant Run 拆分多个split APK 温启动 类/资源
各方案优缺点:
AndFix:不需要重启,但兼容性差(ART不同版本ArtMethod结构不同)
Tinker:稳定可靠,但需要冷启动
Robust:兼容性好,但包体积增加(每个方法都插入了路由逻辑)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 16.总结与技术思考
# 16.1 核心要点回顾
Android虚拟机与类加载核心要点:
1. ART是Android当前的运行时
└── 混合编译:解释执行 + JIT + AOT
└── Profile-guided编译是最佳实践
2. DEX是Android专用的字节码格式
└── 基于寄存器的指令集
└── 共享常量池,比JAR更紧凑
3. 类加载采用双亲委派模型
└── BootClassLoader → PathClassLoader
└── DexPathList.dexElements[]是核心数据结构
4. dexElements的顺序决定类加载优先级
└── 这是MultiDex和热修复的基础
5. ART的执行方式决定性能
└── AOT代码 > JIT编译 > 解释执行
└── 内联缓存优化虚方法调用
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 16.2 面试高频问题
问题:PathClassLoader和DexClassLoader的区别?
- Android 8.0之前,DexClassLoader可以指定optimizedDirectory,PathClassLoader不行
- Android 8.0+两者已无本质区别,optimizedDirectory参数被忽略
问题:MultiDex的原理?
- 将多个DEX文件打包到APK中
- 运行时通过反射将所有DEX的Element添加到dexElements数组
- ART原生支持,Dalvik需要Support Library
问题:热修复为什么把patch.dex放在前面?
- dexElements按顺序查找类
- 放在前面的DEX优先被搜索到
- 修复后的类会覆盖有Bug的类
问题:ART的混合编译是怎么工作的?
- 安装时只验证DEX,不编译
- 运行时使用解释器+JIT
- JIT编译热点代码并记录Profile
- 空闲时根据Profile进行选择性AOT编译
# 16.3 学习建议
理解虚拟机和类加载需要分层学习:
- 应用层:理解ClassLoader的使用、双亲委派、热修复原理
- 框架层:理解DexPathList、DexFile的实现
- 运行时层:理解ART的编译策略、GC机制、对象模型
- 编译器层:理解dex2oat的优化Pipeline
建议从ClassLoader出发,先理解类如何被加载,再深入理解DEX如何被编译和执行。