第6章:Android Runtime (ART)

Android Runtime (ART) 是Android系统的核心执行环境,负责运行所有Java/Kotlin编写的应用程序。自Android 5.0 (Lollipop) 开始,ART完全取代了Dalvik虚拟机,带来了显著的性能提升和更先进的运行时优化能力。本章将深入剖析ART的内部实现机制,包括DEX文件格式、编译策略、垃圾回收机制,并与iOS运行时进行技术对比,帮助读者全面理解Android应用的执行原理。

6.1 ART架构演进

6.1.1 从Dalvik到ART的转变

Dalvik虚拟机采用JIT (Just-In-Time) 编译模式,在应用运行时将字节码编译为机器码。这种方式导致:

  • 应用启动速度较慢(需要解释执行或等待JIT编译)
  • 运行时编译消耗CPU和电池(编译过程与应用执行竞争资源)
  • 热点代码需要重复编译(每次启动都需要重新识别和编译)
  • 内存占用较高(需要保存JIT编译器和编译缓存)

ART引入了AOT (Ahead-Of-Time) 编译,在应用安装时就将DEX字节码编译为本地机器码,存储在OAT文件中。主要优势包括:

  • 应用启动速度提升30-50%(省去JIT编译时间)
  • 运行时性能更佳(直接执行优化后的机器码)
  • 降低CPU使用率和功耗(无需运行时编译)
  • 更好的电池续航(减少了CPU密集型操作)

Dalvik到ART的技术挑战:

  1. 兼容性保证: - 保持Java语义的完全兼容 - 支持所有Dalvik字节码指令 - 处理动态加载和反射场景 - 兼容NDK和JNI调用

  2. 安装时间权衡: - AOT编译增加安装时间 - 需要平衡编译优化级别 - 大型应用可能需要数分钟编译 - 后续引入后台编译缓解此问题

  3. 存储空间考虑: - OAT文件占用额外存储空间 - 典型应用增加50-100%存储占用 - 需要定期清理无用的OAT文件 - Android 7.0后通过混合编译改善

6.1.2 ART核心组件

ART运行时主要包含以下组件:

  1. DEX文件处理器: - 解析和验证DEX文件格式 - 处理多DEX文件情况 - 优化DEX文件布局(dexlayout) - 管理DEX文件内存映射

  2. 编译器驱动(Compiler Driver): - 协调AOT/JIT编译过程 - 管理编译任务队列 - 处理编译依赖关系 - 控制并行编译线程

  3. Optimizing编译器: - 构建中间表示(IR) - 执行多轮优化pass - 生成高质量机器码 - 支持多种目标架构(ARM, x86, MIPS)

  4. 垃圾回收器(GC): - 管理堆内存分配和回收 - 支持多种GC算法 - 并发标记和清理 - 内存压缩和整理

  5. 运行时服务: - 反射API实现 - JNI接口层 - 调试器支持(JDWP) - 性能采样(Profiling)

  6. 类加载器(Class Loader): - 动态加载类和资源 - 管理类加载器层次 - 处理类的初始化顺序 - 支持自定义类加载器

6.1.3 编译策略演进

ART的编译策略经历了多次重要演进:

  • Android 5.0-6.0(纯AOT时代):
  • 安装时编译所有代码
  • 优化级别固定
  • 安装时间长,但运行性能好
  • 存储占用大

  • Android 7.0+(混合编译引入):

  • JIT编译器回归
  • Profile-guided编译
  • 平衡安装速度和运行性能
  • 智能选择编译目标

  • Android 8.0+(编译优化增强):

  • 引入dexlayout优化DEX文件布局
  • VDEX(Verified DEX)文件
  • 改进的内联策略
  • 更快的安装和更新

  • Android 9.0+(云端智能):

  • 云端配置文件(Cloud Profiles)支持
  • Google Play收集和分发Profile
  • 首次安装即可获得优化
  • 减少设备端编译负担

  • Android 10.0+(性能与效率):

  • 改进的垃圾回收和内存管理
  • R8编译器取代ProGuard
  • 更激进的代码优化
  • 降低内存占用

  • Android 11.0+(增量优化):

  • 增量dex2oat支持
  • 更智能的后台编译
  • 改进的启动性能
  • 更低的电量消耗

6.1.4 ART内部架构详解

运行时核心模块:

  1. Class Linker: - 负责类的加载和链接 - 管理类的继承关系 - 处理方法解析和字段访问 - 维护类加载器层次结构

  2. Thread Management: - 线程创建和销毁 - 线程本地存储(TLS) - 线程同步原语实现 - 线程状态转换管理

  3. Memory Management: - 堆内存分配器 - 内存映射管理 - 大对象空间(LOS) - 内存保护和权限控制

  4. Interpreter: - 可切换解释器(Switch/Mterp) - 快速路径优化 - 内置函数(intrinsics)支持 - Safe point检查

  5. Compiler Driver: - 编译任务调度 - 并行编译支持 - 编译选项管理 - Profile数据集成

ART与System Server交互:

ART运行时与Android系统服务紧密集成:

  1. PackageManager集成: - 安装时编译触发 - dexopt服务调用 - 编译状态跟踪 - OTA更新优化

  2. ActivityManager协作: - 进程优先级调整 - 内存压力响应 - 应用生命周期感知 - 后台编译调度

  3. StorageManager配合: - OAT文件存储管理 - 存储空间监控 - 文件权限设置 - 多用户环境支持

6.1.5 ART启动流程深度剖析

Runtime初始化序列:

  1. Early Init阶段: - 命令行参数解析(ParsedOptions处理-Xmx、-Xms、-XX等参数) - 内存映射初始化(MemMap创建匿名映射区域) - 信号处理器安装(SIGSEGV用于空指针检测、SIGUSR1用于GC) - TLS初始化(Thread::InitTlsEntryPoints设置快速路径入口) - 页面保护设置(mprotect设置代码段只读) - CPU特性检测(Runtime::Init检测NEON、SSE等指令集)

  2. Runtime创建: - JavaVMExt实例化(JNI接口实现层) - Heap初始化(配置堆大小、GC算法选择、分代设置) - ThreadList创建(管理所有Java线程、维护全局线程锁) - ClassLinker启动(负责类加载、方法链接、字段偏移计算) - InternTable初始化(字符串常量池管理) - MonitorPool创建(对象锁池预分配)

  3. Boot Class加载: - 加载核心Java类(从boot.oat映射,包含java.lang.*等) - 初始化基础类型(创建Class、Class等镜像类) - 注册JNI方法(RegisterNatives注册系统原生方法) - 创建系统ClassLoader(BootClassLoader和PathClassLoader层次) - WellKnownClasses初始化(缓存常用类如Thread、String的引用) - 校验boot镜像完整性(CheckBootImageContainsClasses)

  4. 线程附加: - 主线程附加到Runtime(Thread::Attach创建Thread对象) - 创建线程对象(分配TLAB、设置栈边界) - 初始化线程本地数据(JNIEnvExt、HandleScope等) - 设置线程优先级(nice值映射到Java优先级) - 注册到ThreadList(全局线程注册表) - 设置线程名称(prctl设置内核可见名称)

关键数据结构:

  1. ArtMethod: - 方法元数据存储(方法头16-24字节,包含declaring_class_、access_flags_) - 入口点指针(解释器/JIT/AOT三个entry_point_from_*指针) - 访问标志和修饰符(public/private/static/final/native等组合) - DEX文件索引(dex_method_index_和dex_file_指针) - HotCode计数器(hotness_count_用于JIT触发) - 方法大小缓存(code_size_避免重复计算) - 快速路径内联缓存(inline_cache_优化虚方法调用)

  2. ArtField: - 字段元数据(declaring_class_、access_flags_、field_dex_idx_) - 偏移量计算(offset_成员,实例字段相对对象头的偏移) - 类型信息(通过field_dex_idx_查找类型描述符) - 访问权限(volatile/transient/final等修饰符) - 静态字段存储(静态字段值存储在Class对象中) - 字段读写屏障(支持并发GC的读写屏障)

  3. DexCache: - DEX文件缓存(避免重复解析DEX文件) - 字符串缓存数组(缓存已解析的String对象) - 类型缓存数组(缓存已加载的Class对象) - 方法缓存数组(缓存已链接的ArtMethod) - 字段缓存数组(缓存已解析的ArtField) - 预解析字符串(preresolved_strings_提前解析常用字符串) - 弱引用清理(配合GC清理无用缓存项)

  4. LinearAlloc: - 只读内存分配器(分配后通过mprotect设置只读) - 存储类元数据(ArtMethod、ArtField、vtable、iftable等) - 内存保护机制(PROT_READ保护,防止运行时修改) - 跨进程共享支持(通过ashmem实现zygote共享) - ArenaAllocator实现(基于Arena的快速分配) - 内存使用统计(跟踪各类型元数据的内存占用)

  5. OatFile结构: - OAT文件头(魔数'oat\n'、版本号、校验和) - DEX文件偏移表(多个DEX文件的位置信息) - 编译后代码段(.text段存储机器码) - 方法元数据(OatMethod包含代码偏移、frame大小等) - GC映射表(记录寄存器中的对象引用) - 异常处理表(编译后的异常处理信息)

6.2 DEX文件格式与优化

6.2.1 DEX文件结构剖析

DEX (Dalvik Executable) 是Android特有的字节码格式,相比Java的class文件具有更高的存储效率。一个DEX文件包含以下主要部分:

文件头(Header):

  • 魔数:dex\n035\0(Android 5.0-)、dex\n037\0(Android 7.0+)、dex\n038\0(Android 8.0+)、dex\n039\0(Android 9.0+)
  • 校验和(checksum):Adler-32算法,验证文件完整性
  • SHA-1签名:除魔数、校验和、签名本身外的所有内容
  • 文件大小:整个DEX文件的字节数
  • 头部大小:固定为0x70字节
  • 字节序标记:0x12345678或0x78563412
  • 各数据区的偏移和大小

字符串池(String Pool):

  • 存储所有字符串常量(类名、方法名、字段名、常量字符串等)
  • 使用MUTF-8编码(Modified UTF-8,支持空字符)
  • 通过索引引用,避免重复存储
  • 字符串按字典序排序,支持二分查找
  • 包含字符串数据偏移表和实际字符串数据

类型池(Type Pool):

  • 存储所有类型描述符
  • 包括基本类型和对象类型
  • 基本类型:V(void), Z(boolean), B(byte), S(short), C(char), I(int), J(long), F(float), D(double)
  • 对象类型:Ljava/lang/String;, [I(int数组), [[Ljava/lang/Object;(二维Object数组)
  • 类型ID按字符串池索引排序

原型池(Proto Pool):

  • 方法签名信息(不包含方法名)
  • 参数类型列表和返回类型
  • 用于方法调用的类型检查和方法重载解析
  • 相同签名的方法共享同一原型

字段池(Field Pool):

  • 所有字段的定义
  • 包含所属类索引、类型索引、名称索引
  • 按所属类、名称、类型的顺序排序
  • 支持快速字段查找

方法池(Method Pool):

  • 所有方法的定义
  • 包含所属类索引、原型索引、名称索引
  • 按所属类、名称、原型的顺序排序
  • 包括虚方法和直接方法

类定义(Class Definitions):

  • 类的结构信息
  • 类索引、访问标志、父类索引、接口列表偏移
  • 源文件名索引(用于调试)
  • 注解信息偏移
  • 类数据偏移(字段和方法的详细信息)
  • 静态字段初始值偏移

类数据(Class Data):

  • 使用LEB128编码节省空间
  • 静态字段列表
  • 实例字段列表
  • 直接方法列表(构造函数、私有方法、静态方法)
  • 虚方法列表(可重写的方法)

代码区(Code Area):

  • 方法的字节码指令(2字节对齐的指令流)
  • 寄存器数量(registers_size,包含局部变量和参数,最大65536)
  • 输入参数数量(ins_size,方法参数占用的寄存器数)
  • 输出参数数量(outs_size,调用其他方法时的最大参数数)
  • 尝试块数量(tries_size,try-catch块的数量)
  • 调试信息偏移(debug_info_off,指向调试信息的偏移)
  • 指令列表(insns,实际的DEX指令数组)
  • 异常处理表(tries和handlers,异常处理的范围和跳转目标)
  • 填充字节(padding,保证下一个方法4字节对齐)
  • 局部变量信息(通过debug_info间接访问)

数据区(Data Section):

  • 存储各种辅助数据
  • 注解(Annotations)
  • 调试信息(Debug Info)
  • 编码数组(Encoded Arrays)
  • 类静态值(Static Values)

链接数据(Link Data):

  • 用于动态链接的信息
  • 通常为空,保留用于未来扩展

6.2.2 DEX优化技术

  1. 常量池合并

DEX格式通过共享常量池显著减少文件大小:

  • 相同字符串只存储一次(如多个类使用"toString"方法名)
  • 类型描述符去重(所有String类型共享"Ljava/lang/String;")
  • 方法签名复用(相同参数和返回类型的方法共享原型)
  • 典型应用可减少35-40%的存储空间

实际优化案例:

// Java代码中的重复
class A { String name; void setName(String s) {} }
class B { String title; void setTitle(String s) {} }

// DEX中的优化存储
字符串池: ["name", "title", "setName", "setTitle", "Ljava/lang/String;", "V"]
类型池: [String类型索引]
原型池: [(String)->void 仅存储一次]
  1. 寄存器架构优化

与Java虚拟机的栈架构不同,Dalvik/ART采用寄存器架构:

  • 减少指令数量(约少30%)
  • 减少内存访问(寄存器访问比栈访问快)
  • 提高解释执行效率
  • 更适合移动设备的CPU架构(ARM本身是寄存器架构)

架构对比示例:

// Java字节码(栈架构)- 计算 a + b
iload_1      // 加载变量a到栈
iload_2      // 加载变量b到栈
iadd         // 弹出两个值,相加,结果压栈
istore_3     // 弹出结果存储到变量c

// DEX字节码(寄存器架构)- 计算 a + b
add-int v3, v1, v2  // 直接将v1和v2相加,结果存入v3
  1. 指令集优化

DEX指令集针对移动场景优化:

  • 专门的数组操作指令(aget, aput, aget-wide, aget-object等)
  • 优化的方法调用指令(invoke-virtual/quick, invoke-virtual/range)
  • 紧凑的指令编码(1-5个16位字)
  • 特殊的常量加载指令(const/4, const/16, const/high16)

指令编码优化:

  • 高频指令使用短编码
  • 支持多种操作数范围(4位、8位、16位、32位)
  • 寄存器范围优化(v0-v15使用短编码)
  1. 16位指令设计

DEX使用16位指令字,优势包括:

  • 更好的内存对齐(ARM架构友好,避免非对齐访问)
  • 减少指令缓存占用(I-Cache利用率提高40%)
  • 简化解码逻辑(固定位置提取操作码和操作数)
  • 适合16位Thumb指令集(ARM Thumb模式完美匹配)
  • 支持紧凑编码(小常量直接嵌入指令)
  1. 字符串去重优化

DEX通过全局字符串池实现高效去重:

  • 编译时字符串内化(所有相同字符串共享一个条目)
  • 运行时字符串常量池(String.intern()直接映射)
  • UTF-8/UTF-16转换优化(缓存常用字符串的转换结果)
  • 字符串比较加速(相同引用直接返回true)
  1. 方法内联提示

DEX格式支持编译优化提示:

  • @inline注解支持(强制内联小方法)
  • getter/setter自动识别(单行return/赋值语句)
  • 热点方法标记(基于Profile的内联决策)
  • 跨DEX内联支持(打破模块边界)
  1. 类层次优化

利用类继承关系优化存储:

  • vtable压缩(相同方法签名共享槽位)
  • iftable去重(接口方法表合并)
  • 超类方法引用(避免重复存储继承的方法)
  • 类初始化优化(方法合并)

6.2.3 DEX布局优化(dexlayout)

Android 8.0引入dexlayout工具,根据运行时profile重新组织DEX文件:

热点代码聚集:

  • 将频繁执行的方法放在一起
  • 减少内存页面切换(通常4KB页面)
  • 提高CPU缓存命中率(L1/L2缓存)
  • 典型应用启动时间减少15-20%

布局优化策略:

  1. 启动时序分析: - 记录应用启动路径 - 识别启动关键类 - 优先排列初始化代码

  2. 空间局部性优化: - 相关类和方法聚集 - 调用链上的方法相邻存放 - 减少跨页面跳转

  3. 时间局部性优化: - 热点循环代码集中 - 频繁调用的工具方法优先 - 事件处理代码聚集

冷代码分离:

  • 错误处理代码移到文件末尾
  • 调试相关代码分离
  • 很少使用的功能模块后置
  • 减少工作集大小

启动类优先:

  • Application类最优先
  • 主Activity及其依赖类
  • 常用Service和BroadcastReceiver
  • 初始化所需的工具类

Profile数据来源:

  • 开发者本地测试
  • Google Play用户数据聚合
  • 设备端运行时收集
  • 自动化测试覆盖率

6.2.4 Multi-DEX处理

由于DEX格式限制,单个DEX文件最多包含65536个方法引用。大型应用需要使用Multi-DEX:

65K方法限制的原因:

  • DEX格式使用16位索引引用方法
  • 2^16 = 65536个方法引用上限
  • 包括应用自身代码和所有依赖库
  • 常见于集成多个SDK的大型应用

构建时分包策略:

  1. 主DEX确定: - Application类必须在主DEX - Application直接引用的类 - 启动过程必需的类 - ContentProvider及其依赖

  2. 依赖分析: - 构建类依赖图 - 识别启动关键路径 - 最小化主DEX大小 - 避免类加载失败

  3. 分包算法: - 贪心算法填充DEX文件 - 保持相关类在同一DEX - 平衡各DEX文件大小 - 考虑类加载顺序

运行时加载策略:

  1. Dalvik时代(Android 4.4-):
MultiDex.install(context) 流程:

1. 提取APK中的次要DEX文件
2. 使用dexopt优化DEX文件
3. 创建DexClassLoader实例
4. 修改PathClassLoader的dexElements数组
5. 合并类加载器搜索路径
  1. 加载优化技术: - 后台线程预加载 - 增量式DEX提取 - 优化文件缓存策略 - 并行dexopt处理

  2. 性能影响: - 首次启动时间增加(提取和优化) - 内存占用增加(多个DexFile对象) - 类查找性能下降(遍历多个DEX)

Android 5.0+原生支持:

  • ART原生支持多DEX文件,无需MultiDex库
  • OAT文件格式原生支持多DEX
  • 统一的类查找和加载机制
  • 安装时合并优化所有DEX

Multi-DEX最佳实践:

  1. 减少方法数: - 使用ProGuard/R8移除无用代码 - 按需引入库的特定模块 - 避免引入臃肿的第三方库 - 定期审查依赖

  2. 优化主DEX: - 使用multiDexKeepFile配置 - 手动指定主DEX包含的类 - 延迟初始化非关键组件 - 最小化启动依赖

  3. 构建配置:

android {
  defaultConfig {
    multiDexEnabled true
    multiDexKeepFile file('multidex-keep.txt')
  }
}

6.2.5 与Java字节码的对比

| 特性 | Java字节码 | DEX字节码 |

特性 Java字节码 DEX字节码
架构 基于栈 基于寄存器
文件组织 每个类一个.class文件 所有类在一个.dex文件
常量池 每个类独立常量池 全局共享常量池
指令数量 约200条指令 约230条指令
指令长度 1-3字节变长 2-10字节(16位对齐)
类型信息 指令中包含类型 寄存器无类型,运行时推断
方法调用 invokevirtual等 invoke-virtual/direct/static等
异常处理 异常表在方法末尾 与代码交织存储
调试信息 LineNumberTable等属性 专门的调试信息格式
优化程度 javac基本不优化 dx/d8进行优化转换

指令集设计差异:

  1. 操作数模型: - Java:操作数在操作数栈上 - DEX:操作数在虚拟寄存器中

  2. 方法调用约定: - Java:参数压栈,返回值在栈顶 - DEX:参数在寄存器,返回值在v0

  3. 常量处理: - Java:ldc指令加载常量池 - DEX:const系列指令直接编码常量

  4. 类型安全: - Java:强类型指令(iadd, fadd等) - DEX:部分类型合并(add-int处理int和float)

性能特征对比:

| 方面 | Java字节码 | DEX字节码 |

方面 Java字节码 DEX字节码
代码密度 较低 高30-35%
内存占用 较高(栈帧大) 较低(寄存器分配)
解释执行速度 较慢 快20-30%
验证复杂度 复杂(类型推导) 简单(提前验证)
JIT友好度 一般 更好(寄存器分配)

6.3 AOT/JIT编译策略

Android 7.0 (Nougat) 开始,ART采用了混合编译模式,结合AOT和JIT的优势,实现了更智能的编译策略。这种策略在应用性能、安装速度和存储空间之间达到了更好的平衡。

6.3.1 Profile-Guided Compilation (PGC)

配置文件引导编译是ART混合编译的核心,通过收集应用的实际运行数据来指导编译决策。

Profile收集机制:

  1. 运行时采样: - JIT编译器记录热点方法 - 采样频率动态调整 - 低开销的profile收集

  2. Profile文件格式: - 存储在/data/misc/profiles/ - 包含热点方法、类信息 - 定期合并和更新

  3. 云端Profile (Android 9.0+): - Google Play收集用户Profile - 聚合分析生成通用Profile - 随APK分发,优化首次安装

热点检测算法:

  • 方法调用计数器
  • 循环回边计数
  • 基于阈值的热点判定
  • 考虑方法大小和复杂度

6.3.2 AOT编译流程

dex2oat工具链:

dex2oat是ART的AOT编译器,负责将DEX字节码转换为OAT (Optimized Android file format) 文件。

主要步骤:

  1. DEX文件解析和验证
  2. 构建中间表示(IR)
  3. 执行优化passes
  4. 生成目标架构机器码
  5. 创建OAT文件

编译过滤器(Compilation Filters):

ART提供多种编译级别,通过--compiler-filter参数控制:

  • verify:仅验证DEX代码
  • quicken:快速优化,仅做基本优化
  • speed-profile:基于Profile编译热点代码
  • speed:编译所有方法,最大优化
  • everything:编译所有方法和类初始化器

编译优化技术:

  1. 内联(Inlining): - 将小方法直接嵌入调用点 - 减少方法调用开销 - 启用更多优化机会

  2. 逃逸分析(Escape Analysis): - 分析对象生命周期 - 栈上分配优化 - 消除不必要的同步

  3. 循环优化: - 循环展开 - 循环向量化 - 边界检查消除

  4. 死代码消除: - 移除不可达代码 - 常量折叠 - 条件简化

6.3.3 JIT编译机制

分层编译模型:

ART的JIT采用分层编译策略:

  1. 解释执行层: - 初始执行使用解释器 - 收集运行时信息 - 最小内存占用

  2. JIT编译层: - 识别热点方法 - 后台编译线程 - 生成优化的机器码

  3. OSR (On-Stack Replacement): - 长时间运行的循环优化 - 从解释模式切换到编译代码 - 保持执行状态一致性

代码缓存管理:

JIT编译的代码存储在内存中的代码缓存:

  • 缓存大小限制:
  • 根据设备内存动态调整
  • 典型大小:2-8MB
  • LRU淘汰策略

  • 缓存组织:

  • 代码区:存储编译后的机器码
  • 数据区:存储元数据和跳转表
  • ProfilingInfo:运行时统计信息

  • 缓存持久化:

  • 定期保存到磁盘
  • 下次启动时加载
  • 减少重复编译

6.3.4 混合编译策略演进

Android 7.0 策略:

  • 安装时仅验证
  • 运行时JIT编译
  • 空闲时AOT编译

Android 8.0 改进:

  • dexlayout优化
  • 更智能的编译触发
  • VDEX (Verified DEX) 引入

Android 9.0 优化:

  • 云端Profile支持
  • 改进的后台编译
  • 更快的应用更新

Android 10+ 增强:

  • 增量式dex2oat
  • 更好的内存管理
  • R8编译器集成

6.3.5 编译决策因素

ART在决定何时以及如何编译代码时,考虑以下因素:

  1. 设备状态: - 充电状态 - 空闲时间 - 可用存储空间 - 温度限制

  2. 应用特征: - 使用频率 - 代码复杂度 - 启动性能要求 - 更新频率

  3. 系统资源: - CPU使用率 - 内存压力 - 电池电量 - 并发任务

6.4 垃圾回收机制

ART的垃圾回收器(GC)是保证Android应用内存效率的关键组件。相比Dalvik的单一GC算法,ART提供了多种GC策略,可以根据不同场景选择最优方案。

6.4.1 ART中的GC算法

  1. Concurrent Mark Sweep (CMS)

CMS是ART的主要GC算法,特点是并发执行,减少应用暂停时间:

  • 标记阶段:
  • 初始标记:暂停应用,标记GC Roots
  • 并发标记:与应用并发运行,遍历对象图
  • 重新标记:短暂暂停,处理并发期间的变化

  • 清理阶段:

  • 并发清理死亡对象
  • 内存整理(可选)
  • 更新分配指针

  • 写屏障(Write Barrier):

  • 追踪并发标记期间的引用变化
  • 使用卡表(Card Table)记录脏页
  • 确保标记的正确性
  1. Generational Collection

分代收集基于"大部分对象都是短命的"这一观察:

  • Young Generation(新生代):
  • 存放新分配的对象
  • 使用复制算法收集
  • 收集频率高,暂停时间短

  • Old Generation(老年代):

  • 存放长期存活的对象
  • 使用CMS或压缩收集
  • 收集频率低,但时间较长

  • 晋升策略:

  • 对象存活次数计数
  • 达到阈值后晋升到老年代
  • 大对象直接分配到老年代
  1. Region-based Collection (CC)

Concurrent Copying (CC) 是ART较新的GC算法:

  • 区域划分:
  • 堆分为固定大小的区域
  • 每个区域可独立回收
  • 支持增量式收集

  • 并发复制:

  • 使用读屏障(Read Barrier)
  • 对象访问时动态转发
  • 减少GC暂停时间

  • 压缩效果:

  • 自动内存压缩
  • 消除内存碎片
  • 提高内存利用率

6.4.2 内存分配机制

  1. TLAB (Thread Local Allocation Buffer)

每个线程维护私有的分配缓冲区:

  • 避免分配时的锁竞争
  • 快速的bump pointer分配
  • TLAB耗尽时从堆申请新块
  1. Bump Pointer分配

在连续内存区域中顺序分配:

  • 仅需移动指针
  • O(1)时间复杂度
  • 配合压缩GC使用
  1. Free List分配

在有碎片的堆中分配:

  • 维护空闲块链表
  • 首次适配或最佳适配
  • 适用于CMS等非压缩GC
  1. Large Object Space (LOS)

大对象专用空间:

  • 默认阈值:12KB
  • 独立的分配和回收策略
  • 避免新生代空间碎片化

6.4.3 GC触发机制

1. 分配失败触发: - 内存分配请求无法满足 - 触发同步GC - 可能导致OOM

2. 堆增长触发: - 堆使用率超过阈值 - 触发并发GC - 阈值动态调整

3. 显式请求: - System.gc()调用 - 调试和测试用途 - 生产环境应避免

4. 后台GC: - 应用切换到后台 - 更激进的回收策略 - 释放更多内存给前台应用

6.4.4 GC性能调优

1. 堆大小配置: - dalvik.vm.heapstartsize:初始堆大小 - dalvik.vm.heapgrowthlimit:应用堆增长限制 - dalvik.vm.heapmaxfree:最大空闲堆 - dalvik.vm.heaptargetutilization:目标利用率

2. GC日志分析: - 通过logcat查看GC日志 - 分析GC原因、耗时、回收量 - 识别内存泄漏和性能问题

3. 内存压力处理: - onTrimMemory()回调 - 主动释放缓存 - 降级服务质量

6.4.5 引用类型处理

ART支持Java的各种引用类型:

1. 强引用(Strong Reference): - 普通对象引用 - GC不会回收 - 可能导致内存泄漏

2. 软引用(Soft Reference): - 内存不足时回收 - 适合实现缓存 - SoftReference<T>类

3. 弱引用(Weak Reference): - GC时即可回收 - 用于防止内存泄漏 - WeakReference<T>类

4. 虚引用(Phantom Reference): - 对象回收时通知 - 配合引用队列使用 - PhantomReference<T>类

5. Finalizer处理: - finalize()方法执行 - FinalizerDaemon线程 - 影响GC性能,应避免使用

6.5 与iOS运行时对比

理解ART与iOS运行时的差异,有助于深入把握两大移动平台的技术特点和设计理念。

6.5.1 内存管理模型对比

| 特性 | Android ART | iOS Runtime |

特性 Android ART iOS Runtime
内存管理 自动垃圾回收 (GC) 自动引用计数 (ARC)
开发者负担 较低,自动管理 中等,需要理解引用循环
内存释放时机 GC周期性回收 引用计数归零立即释放
暂停时间 存在GC暂停 无全局暂停
内存碎片 GC可压缩整理 可能产生碎片
性能可预测性 GC时机不确定 释放时机确定

ARC的优势:

  • 内存释放及时,占用更少
  • 无GC暂停,响应更流畅
  • 性能更可预测

GC的优势:

  • 自动处理循环引用
  • 开发更简单,出错更少
  • 支持更复杂的内存模式

6.5.2 方法调度机制

ART方法调度:

  • 虚方法表(vtable)实现
  • 接口方法表(itable)
  • 内联缓存优化
  • 编译时去虚化

iOS方法调度:

  • Objective-C:消息发送机制
  • objc_msgSend动态派发
  • 方法缓存优化
  • 运行时method swizzling
  • Swift:静态派发为主
  • final和private方法直接调用
  • 协议见证表(witness table)
  • Whole Module Optimization

性能对比:

  • 静态派发:Swift > Java/Kotlin
  • 动态派发:ART虚方法 > Objective-C消息
  • 优化潜力:ART编译优化 > iOS运行时优化

6.5.3 类型系统差异

ART类型系统:

  • 基于Java类型系统
  • 泛型类型擦除
  • 基本类型与对象类型分离
  • 强类型检查

iOS类型系统:

  • Objective-C:动态类型
  • id类型运行时解析
  • 鸭子类型支持
  • Swift:静态强类型
  • 泛型完整保留
  • 值类型支持
  • 协议导向编程

6.5.4 启动性能对比

Android应用启动:

  1. 进程创建(fork from Zygote)
  2. 加载应用代码
  3. 执行Application初始化
  4. 创建主Activity
  5. 布局加载和渲染

iOS应用启动:

  1. 进程创建(全新进程)
  2. 动态链接器加载
  3. Runtime初始化
  4. main()函数执行
  5. UIApplication初始化

启动优化技术:

  • Android:
  • 预加载类和资源(Zygote)
  • AOT编译优化
  • 启动画面预显示
  • iOS:
  • 二进制重排
  • 延迟加载动态库
  • 启动闭包缓存

6.5.5 运行时特性对比

反射能力:

  • ART:完整的Java反射API
  • iOS:Objective-C运行时API,Swift有限反射

动态特性:

  • ART:类加载器,动态代理
  • iOS:运行时方法添加/替换

调试支持:

  • ART:JDWP协议,ADB调试
  • iOS:LLDB调试器,Instruments

性能分析:

  • ART:Systrace, Simpleperf
  • iOS:Time Profiler, Allocations

6.5.6 安全机制对比

代码签名:

  • Android:APK签名,可自签名
  • iOS:强制代码签名,需开发者证书

运行时保护:

  • ART:DEX加密,代码混淆
  • iOS:加密内存页,地址随机化

权限模型:

  • Android:安装时/运行时权限
  • iOS:首次使用时请求权限

本章小结

本章深入剖析了Android Runtime (ART)的核心机制,从架构演进到具体实现细节。关键要点包括:

  1. ART架构演进:从Dalvik的JIT到ART的AOT,再到混合编译模式,体现了在性能、安装时间、存储空间之间寻求平衡的设计理念。

  2. DEX文件格式:作为Android特有的字节码格式,通过寄存器架构、常量池共享、16位指令等设计,实现了比Java字节码更高的存储和执行效率。

  3. 编译策略:Profile-Guided Compilation结合AOT和JIT的优势,通过运行时数据收集和云端Profile分发,实现了智能化的编译优化。

  4. 垃圾回收机制:从CMS到CC,ART提供了多种GC算法,在暂停时间、吞吐量、内存占用之间提供了灵活的选择。

  5. 与iOS对比:ART的GC vs iOS的ARC,各有优劣。理解两种内存管理模型的差异,有助于开发跨平台应用时做出正确的设计决策。

关键公式和概念:

  • DEX方法数限制:2^16 = 65,536
  • 寄存器架构指令数减少:约30%
  • 热点代码聚集性能提升:15-20%
  • GC暂停时间:CMS < 10ms,CC < 5ms
  • 内存占用:ART = DEX + OAT ≈ 2.5 × DEX

练习题

基础题

  1. DEX文件结构理解 解释为什么DEX文件比多个class文件更适合移动设备?列举至少4个优势。
提示 考虑存储效率、内存映射、常量池共享、I/O操作等方面。
答案 DEX文件相比class文件的优势: 1. 存储效率:通过全局常量池共享,相同的字符串、类型、方法签名只存储一次,减少35-40%存储空间 2. 内存映射友好:单个文件可以直接mmap到内存,减少内存分配和拷贝 3. I/O优化:加载一个DEX文件比加载数百个class文件的I/O开销小得多 4. 寄存器架构:指令数量减少约30%,更适合ARM等RISC架构 5. 启动优化:减少文件系统访问,加快类加载速度 6. 验证优化:DEX在构建时完成部分验证,运行时验证更快
  1. ART编译模式选择 在什么场景下应该使用speed-profile编译过滤器,什么场景下使用speed?
提示 考虑应用类型、更新频率、存储限制等因素。
答案 使用speed-profile的场景: - 普通用户应用(游戏、社交、工具类) - 存储空间有限的设备 - 需要快速安装和更新 - 应用有明显的热点代码路径 使用speed的场景: - 系统核心应用(Launcher、Settings) - 性能关键型应用(相机、输入法) - 企业定制设备的预装应用 - 不常更新的应用
  1. GC算法选择 比较CMS和CC两种GC算法的适用场景。
提示 从暂停时间、CPU开销、内存碎片等维度分析。
答案 CMS (Concurrent Mark Sweep): - 适合内存充足的设备 - 对CPU资源要求较低 - 可能产生内存碎片 - 暂停时间较短但不稳定 CC (Concurrent Copying): - 适合需要低延迟的应用(游戏、视频) - CPU开销较大(读屏障) - 自动内存压缩,无碎片 - 暂停时间更短且稳定
  1. Multi-DEX优化 如何减少Multi-DEX对应用启动性能的影响?
提示 考虑主DEX内容、加载时机、构建优化等。
答案 优化策略: 1. 最小化主DEX:只包含Application类和启动必需类 2. 使用multiDexKeepFile精确控制主DEX内容 3. 延迟加载次要DEX:在闪屏页面后台加载 4. 代码瘦身:使用R8/ProGuard移除无用代码 5. 模块化:按功能拆分,按需加载 6. 预加载优化:在Application中预热关键类

挑战题

  1. 性能分析题 某应用启动时间过长,如何通过ART相关工具诊断问题?设计一个完整的诊断流程。
提示 考虑使用systrace、simpleperf、Profile数据等工具。
答案 诊断流程: 1. 使用systrace捕获启动过程: - 查看dex2oat是否在启动时运行 - 检查类加载和验证时间 - 分析JIT编译开销 2. 检查编译状态: - `adb shell cmd package compile -m speed-profile <package>` - 查看`/data/misc/profiles/`下的profile文件 3. 分析DEX布局: - 使用dexdump查看DEX文件结构 - 检查启动类是否在主DEX - 验证dexlayout是否生效 4. 内存和GC分析: - 查看GC日志,检查启动时GC频率 - 使用`dumpsys meminfo`查看内存使用 5. 优化建议: - 应用Profile-guided优化 - 调整Multi-DEX策略 - 优化类加载顺序
  1. 设计题 设计一个自定义类加载器,实现动态加载加密的DEX文件,需要考虑哪些ART相关的技术细节?
提示 考虑DEX格式验证、内存映射、类加载器层次、GC影响等。
答案 技术考虑点: 1. DEX解密和验证: - 在内存中解密DEX数据 - 调用DexFile.loadDex验证格式 - 处理OdexFile生成 2. 类加载器实现: - 继承BaseDexClassLoader - 实现findClass方法 - 维护已加载类缓存 3. 内存管理: - 使用DirectByteBuffer减少拷贝 - 及时释放解密后的原始数据 - 考虑对GC的影响 4. 安全考虑: - 防止内存dump - 动态解密关键类 - 混淆类名和方法名 5. 性能优化: - 预解密常用类 - 支持增量加载 - 缓存编译后的代码
  1. 优化题 如何设计一个ART友好的序列化框架?考虑DEX格式和运行时特性。
提示 思考反射开销、方法数限制、内联优化等因素。
答案 设计要点: 1. 避免运行时反射: - 使用注解处理器生成代码 - 编译时生成序列化方法 - 利用方法句柄(MethodHandle) 2. 优化方法数: - 生成通用序列化方法 - 使用内部类减少公开API - 支持增量编译 3. ART优化友好: - 生成可内联的getter/setter - 避免虚方法调用 - 使用final类和方法 4. 内存效率: - 对象池减少GC压力 - 使用基本类型数组 - 支持流式处理 5. 特定优化: - 利用Unsafe加速访问 - 缓存字段偏移量 - 批量处理减少JNI调用
  1. 开放思考题 如果你来设计下一代Android运行时,会如何改进现有的ART?
提示 可以从AI加速、内存效率、启动性能、跨平台等角度思考。
答案 可能的改进方向: 1. AI驱动的编译优化: - 基于用户行为的个性化编译 - 神经网络预测热点代码 - 自适应GC策略 2. 内存效率提升: - 更激进的对象压缩 - 跨应用内存共享 - 智能内存预取 3. 启动性能革新: - 持久化JIT缓存 - 增量类加载 - 并行初始化 4. 新硬件支持: - 专用字节码加速器 - 硬件辅助GC - 量子计算就绪 5. 跨平台统一: - 与Fuchsia OS整合 - 支持WASM - 统一的IR表示

常见陷阱与错误 (Gotchas)

  1. Multi-DEX类找不到 - 错误:ClassNotFoundException在启动时发生 - 原因:Application依赖的类不在主DEX中 - 解决:使用multiDexKeepProguard确保关键类在主DEX

  2. OOM但堆未满 - 错误:OutOfMemoryError: Failed to allocate - 原因:大对象空间(LOS)耗尽或内存碎片 - 解决:分析大对象分配,考虑使用对象池

  3. JNI局部引用泄漏 - 错误:JNI ERROR: local reference table overflow - 原因:循环中创建局部引用未释放 - 解决:使用DeleteLocalRef或PushLocalFrame/PopLocalFrame

  4. 类加载死锁 - 错误:应用无响应,线程等待类初始化 - 原因:循环依赖的类初始化 - 解决:避免在静态初始化块中触发其他类加载

  5. Profile数据不生效 - 问题:安装后性能未改善 - 原因:Profile文件格式不匹配或权限问题 - 解决:检查pm dump-profiles输出,确认Profile加载

  6. GC频繁导致卡顿 - 现象:Systrace显示频繁GC - 原因:内存抖动或泄漏 - 解决:使用Allocation Tracker定位问题代码

最佳实践检查清单

设计审查要点

  • [ ] DEX优化
  • [ ] 主DEX只包含启动必需类
  • [ ] 使用R8而非ProGuard进行优化
  • [ ] 启用资源缩减和代码缩减
  • [ ] 定期审查方法数增长

  • [ ] 内存管理

  • [ ] 避免在循环中创建临时对象
  • [ ] 大图片使用inBitmap复用
  • [ ] 及时释放不用的大对象引用
  • [ ] 合理使用软引用实现缓存

  • [ ] 编译优化

  • [ ] 关键路径代码保持简单可内联
  • [ ] 避免过度使用反射
  • [ ] 热点方法避免同步
  • [ ] 使用final优化虚方法调用

  • [ ] 启动性能

  • [ ] Application onCreate保持轻量
  • [ ] 延迟初始化非关键组件
  • [ ] 使用启动器Activity预加载
  • [ ] 避免启动时触发GC

  • [ ] 运行时效率

  • [ ] 使用ArrayMap代替HashMap(小数据集)
  • [ ] 缓存反射结果
  • [ ] 批量JNI调用减少开销
  • [ ] 合理使用本地方法

  • [ ] 调试和监控

  • [ ] 集成StrictMode检测
  • [ ] 添加关键路径性能埋点
  • [ ] 定期分析Profile数据
  • [ ] 监控GC频率和暂停时间