ART虚拟机中的OAT文件及加载

OAT文件内容

在 Android 7.0 (Nougat) 及以后版本中,ART 的混合编译模式改变了 OAT 文件的结构。以下是关键点的详细解释:

  1. OAT 文件内容

    • 包含 AOT 编译的机器码:部分经过 AOT 编译的方法会以原生机器码形式存在。由于这些代码已经是目标平台的机器码,因此在运行时无需再进行字节码到机器码的转换,可以直接执行,从而显著提高了启动速度和运行效率。
    • 保留原始 DEX 字节码:未编译的方法会保留原始的 DEX 字节码(并非完整 DEX 文件,而是按需嵌入)。
    • 元数据和映射信息:记录哪些方法已编译、哪些未编译,以及两者的对应关系。
  2. 混合编译逻辑

    • 安装时部分 AOT:系统会根据设备状态(如充电、空闲)选择性地编译常用方法。
    • 运行时 JIT:执行未编译的方法时触发 JIT 编译(对于频繁执行的热点代码),结果缓存到内存(后可能持久化为 AOT)。
    • Profile-Guided 优化:根据用户实际使用模式(记录在 profile 文件中)逐步优化高频代码。
    • 当设备处于空闲且充电状态时,ART 的编译守护进程(dex2oat)会读取配置文件,将热点代码进行 AOT 编译,并将编译结果保存到 .odex 文件中
  3. 文件结构示例

    OAT 文件
    ├── 已编译方法 (机器码)
    ├── 未编译方法 (DEX 字节码片段)
    ├── 方法调用跳转表
    └── 元数据 (记录编译状态)
    
  4. 版本演进

    • Android 7.0 引入混合模式,解决纯 AOT 导致的安装时间过长和存储占用问题。
    • Android 8.0 进一步优化 JIT 缓存持久化(通过 dex2oat 后台任务)。
  5. 开发者影响

    • 冷启动性能:首次执行未编译方法会有 JIT 开销。
    • 热代码路径:应尽量保持关键方法简洁,便于优化。
    • 调试信息:可通过 adb shell cmd package compile 查看编译状态。

因此,OAT 文件实质是一个分层容器,既包含编译后的机器码,也保留了必要的字节码,通过运行时动态选择执行路径,平衡了安装速度、存储空间和运行效率。

OAT 文件和类的加载

ART 虚拟机在加载 OAT 文件中的类时,总体上遵循 JVM 的类加载阶段(加载→验证→准备→解析→初始化),其中验证,准备,解析也可以认为是连接过程。但由于 ART 的 AOT 编译特性和 Android 运行时优化,具体实现细节与传统 JVM 有显著差异。以下是逐阶段的对比与分析:


1. 加载(Loading)

  • JVM
    • 通过 ClassLoader 查找 .class 文件,读取二进制字节流。
  • ART
    • 输入源不同:直接从 OAT 文件加载(OAT 内嵌了原始 DEX 字节码或已编译的机器码)。
    • 内存映射优化:OAT 文件通过 mmap 映射到内存,避免重复 I/O 操作。
    • 共享机制:多个进程共享同一 OAT 文件的只读代码段(通过 zygote 预加载)。

关键区别
ART 的加载阶段更高效,直接利用预编译的 OAT 文件,而非原始 DEX/Class。


2. 验证(Verification)

  • JVM
    • 在运行时逐条验证字节码的合法性(如类型检查、控制流完整性)。
  • ART
    • AOT 提前验证:在安装或编译时(dex2oat 阶段)完成大部分验证。
    • 运行时轻量级验证:仅对未验证的部分(如动态加载的 DEX)进行补充验证。
    • 优化措施:若 OAT 文件是系统信任的(如系统应用),可能跳过验证。

关键区别
ART 将验证开销从运行时转移到安装/编译时,提升运行时性能。


3. 准备(Preparation)

  • JVM
    • 为类的静态字段分配内存并初始化为默认值(如 int 初始化为 0)。
  • ART
    • 行为一致:同样分配静态字段内存,但可能直接使用 AOT 编译时预计算的结果。
    • 内存布局优化:静态字段的偏移地址在 AOT 编译时已确定,减少运行时计算。

关键区别
ART 利用 AOT 信息优化内存布局,但逻辑与 JVM 相同。


4. 解析(Resolution)

  • JVM
    • 将符号引用(如类名、方法名)转换为直接引用(内存地址)。
  • ART
    • 部分提前解析:AOT 编译时已解析部分符号(如系统类引用)。
    • 延迟解析(Lazy Resolution)
      • 非关键路径的符号引用在首次访问时解析。
      • 通过 “Trampoline”跳转代码 动态绑定目标方法。

关键区别
ART 通过混合解析策略(AOT + 延迟)减少启动开销。


5. 初始化(Initialization)

  • JVM
    • 执行类的 <clinit> 方法(静态代码块初始化)。 类初始化阶段 在 java.lang.Class 层面仍然有锁保护,保证一个类的 <clinit> 方法(静态初始化块)只会执行一次。
  • ART
    • 行为一致:必须执行 <clinit>,但 AOT 编译的 <clinit> 以机器码形式运行更快。
    • 并发优化:Android 8.0+ 支持多线程并发初始化类,避免死锁。
    • ART 内部对 类加载过程 进行了管理,并保证 类的元信息不会被重复创建。

关键区别
逻辑与 JVM 一致,但执行效率更高。


ART 特有的额外阶段

由于 AOT 编译和 Android 运行时优化,ART 在类加载过程中还涉及以下步骤:

1. 机器码绑定(AOT Code Binding)

  • AOT 编译的方法需绑定到当前进程的内存地址(处理 ASLR 重定位)。

2. 编译状态管理

  • 每个方法标记为 AOT/JIT/解释执行状态,运行时动态切换。

3. Profile-Guided 优化(Android 7.0+)

  • 根据运行时采集的性能分析数据(.prof 文件),后台重新优化 OAT 文件。

对比总结

阶段 JVM (Class) ART (OAT)
加载 .class 文件读取 从 OAT 内存映射,嵌入 DEX/机器码
验证 运行时完整验证 大部分在 AOT 编译时完成
准备 分配静态字段内存 同 JVM,但利用 AOT 偏移优化
解析 运行时完全解析 部分 AOT 解析 + 延迟解析
初始化 执行 <clinit> 同 JVM,AOT 机器码更快
额外步骤 机器码绑定、编译状态管理、PGO 优化

为什么 ART 仍需遵循这些阶段?

  1. Java 语言规范要求
    类加载的语义必须符合 JVM 规范(如静态字段初始值、初始化顺序)。
  2. 动态特性支持
    反射、动态代理等功能依赖完整的类加载流程。
  3. 安全性保障
    验证阶段防止恶意字节码破坏运行时。

ART 在加载 OAT 文件时 保留了 JVM 类加载的核心阶段,但通过 AOT 编译和内存映射等技术大幅优化了各阶段的性能。理解这些差异有助于针对 Android 平台优化应用启动速度与内存占用。

类加载器

在 Android ART 虚拟机中,类加载器的设计与标准 JVM 有所不同。以下是完整的类加载器体系及其关系的详细说明,特别澄清 BootstrapClassLoader 的角色以及 Android 特有的实现:


Android ART 中的类加载器完整列表

1. BootClassLoader(核心系统加载器)

  • 作用:加载 Android 框架层的核心类(如 android.*java.*javax.*),对应 libcoreframework 的 OAT 文件。
  • 特点
    • 由 ART 虚拟机内部实现(Java 层不可直接访问,无对应的 Java 类)。
    • 是所有类加载器的父加载器(双亲委派模型的顶端)。
  • 路径
    核心类预编译为 /system/framework/oat/<arch>/boot.oat(如 boot-framework.oat)。

2. PathClassLoader(应用主加载器)

  • 作用:加载已安装 APK 的主 DEX 文件(classes.dex 及优化后的 OAT 文件)。
  • 特点
    • 父加载器是 BootClassLoader
    • 只能加载 /data/app/<package>/ 下的固定路径文件。
  • 使用场景
    应用默认的类加载器,通过 Context.getClassLoader() 获取。

3. DexClassLoader(动态加载器)

  • 作用:加载外部存储的 DEX/JAR/APK 文件(需指定路径)。
  • 特点
    • 父加载器也是 BootClassLoader
    • Android 8.0+ 后受限(需适配 AppComponentFactory)。
  • 使用场景
    插件化、热修复等动态加载技术。

4. InMemoryDexClassLoader(Android 8.0+ 新增)

  • 作用:直接加载内存中的 DEX 字节数组(byte[])。
  • 特点
    • 避免文件写入权限问题,适合安全敏感场景。
    • 父加载器可自定义(通常为 PathClassLoader)。
  • 示例
    byte[] dexBytes = ...; // 从网络或加密文件读取的 DEX 数据
    InMemoryDexClassLoader loader = new InMemoryDexClassLoader(
        ByteBuffer.wrap(dexBytes),
        getClassLoader()
    );
    

5. DelegateLastClassLoader(Android 9.0+ 新增)

  • 作用反向双亲委派(先尝试自己加载,失败后再委托父加载器)。
  • 使用场景
    需要覆盖系统类行为的特殊需求(如兼容性库)。
  • 示例
    DelegateLastClassLoader loader = new DelegateLastClassLoader(
        "/path/to/dex",
        null, // 父加载器(null 表示 BootClassLoader)
        getClassLoader(),
        false // 是否优先加载自身路径
    );
    

与标准 JVM 的对比

类加载器 JVM Android ART
BootstrapClassLoader 加载 rt.jar 等核心库(C++实现) 不存在,由 BootClassLoader 替代
ExtensionClassLoader 加载 ext 目录扩展库 不存在
AppClassLoader 加载用户类路径(-classpath PathClassLoader 替代
  • 关键区别
    Android 没有 BootstrapClassLoaderExtensionClassLoader,其功能由 BootClassLoader 统一实现。

为什么 Android 没有 BootstrapClassLoader?

  1. 设计简化
    Android 的核心类(如 java.lang.String)直接编译在 boot.oat 中,由 BootClassLoader 加载,无需区分 BootstrapExtension
  2. 安全模型
    Android 通过沙箱和权限控制替代 JVM 的 SecurityManager,无需复杂的类加载分层。
  3. 性能优化
    预编译的 OAT 文件减少了运行时解析开销,合并加载器层级可加速类查找。

开发者注意事项

  1. 动态加载的兼容性
    • Android 7.0+ 限制私有 API 访问,动态加载的类可能无法调用系统隐藏 API。
    • Android 11+ 要求 Scoped Storage,外部 DEX 文件需存储到应用专属目录。
  2. 调试类加载器
    // 打印类加载器层次
    ClassLoader cl = MyClass.class.getClassLoader();
    while (cl != null) {
        Log.d("ClassLoader", cl.toString());
        cl = cl.getParent();
    }
    
  3. 自定义类加载器
    • 可继承 BaseDexClassLoader,但需注意 Android 版本差异(如 8.0 后的优化目录策略)。

多线程的情况下,类的加载为什么不会出现重复加载的情况?

总结

Android ART 的类加载器体系是 JVM 的简化版,核心包括:

  • BootClassLoader(系统级)
  • PathClassLoader(应用主路径)
  • DexClassLoader(动态加载)
  • InMemoryDexClassLoader(内存加载)
  • DelegateLastClassLoader(反向委派)

不存在 BootstrapClassLoader,其功能由 BootClassLoader 替代。理解这些加载器的差异和限制,对插件化、热修复等高级开发场景至关重要。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。