第三章 Dalvik 可执行格式与字节码规范

Dalvik 虚拟机

  • Dalvik 虚拟机(Dalvik Virtual Machine):Android 4.4前
  • ART 虚拟机(Android RunTime):Android 4.4开始

Dalvik 虚拟机特点

  • 体积小,占用内存空间小
  • 专有的 DEX(Dalvik Executable)可执行文件格式,体积小,执行速度快
  • 常量池采用 32 位索引值,对类方法名、字段名、常量的寻址速度快
  • 基于寄存器架构,同时拥有一套完整的指令系统
  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理及垃圾回收等重要功能
  • 所有的 Android 程序都运行在 Android 系统进程中,每个进程都与一个 Dalvik 虚拟机实例对应

Dalvik 虚拟机与 Java 虚拟机的区别

运行的字节码不同

  • Java 虚拟机
  • 运行 Java 字节码
  • 程序运行流程:编译 -> 生成 Java 字节码 -> 保存在 class 文件 -> Java 虚拟机解码 class 文件 -> 运行
  • Dalvik 虚拟机
  • 运行 Dalvik 字节码
  • Java 字节码 -> Dalvik 字节码 -> 打包到 Dex 可执行文件 -> Dalvik 虚拟机解释 Dex 文件 -> 运行

Dalvik 可执行文件体积更小

  • Java 字节码转 Dalvik 字节码
  • 由 Android SDK 中的 dx 工具完成
  • dx
  • 重新排列 Java 类文件,消除类文件中所有冗余信息,避免虚拟机初始化时反复加载和解析文件
  • 使用 dx 将 java 文件转为 Dex 的过程
    在这里插入图片描述
  • 如图,基于 dx 对常量池的压缩,相同的字符串和常量在 Dex 文件中只会出现一次(文件体积也就更小)

虚拟机架构不同

  • Java 虚拟机
  • 基于栈结构
  • 程序运行时 Java 虚拟机会频繁对栈进行读写操作,此过程中,不仅会多次进行指令分派与内存访问,且会耗费大量 CPU 时间
  • Dalvik 虚拟机
  • 基于寄存器结构
  • 数据的访问直接在寄存器之间传递(速度快于基于栈的访问方式)

实例

  • 测试代码,保存为 Hello.java
public class Hello {
    public int foo(int a, int b) {
        return (a + b) * (a - b);
    }
    
    public static void main(String[] argc) {
        Hello hello = new Hello();
        System.out.println(hello.foo(5, 3));
    }
}
  • 执行如下命令编译 Java 源文件
javac -source 1.7 -target 1.7 Hello.java
  • 此处规定源码和目标平台字节码都是 Android 1.7 版本的,对应 JDK 7。若本机安装的 JDK 版本高于 7,则必须加上 -source 1.7 和 -target 1.7。对 JDK 8,Android 官方提供了 Jack(Java Android compiler Kit)编译器来生成 Dex 文件

  • 执行如下命令生成 Dex 文件

dx --dex --output=Hello.dex Hello.class
  • 使用 javap 反编译 Hello.class,查看 foo() 函数的 Java 字节码
javap -c -classpath . Hello
  • 执行上述命令后得到如下代码(节选),即 foo() 函数的 Java 字节码
    在这里插入图片描述
  • 使用 dexdump(位于 Android SDK 的 platform-tools 目录中)查看 foo() 函数的 Dalvik 字节码
dexdump -d Hello.dex
  • 执行上述命令后得到如下代码(节选),即 foo() 函数的 Dalvik 字节码
    在这里插入图片描述
分析上述实例
Java 字节码

在这里插入图片描述

  • foo() 函数共占用 8 字节,代码中每条指令占 1 字节且没有参数
  • 这些指令这样存取数据:
  • Java 虚拟机的指令集称零地址形式的指令集,零地址形式指令的源参数和目标参数都是隐含的,通过 Java 虚拟机提供的数据结构“求值栈”来传递
  • 对 Java 程序来说,每个线程在执行时都有一个 PC 计数器和一个 Java 栈
  • PC 计数器以字节为单位记录当前运行位置与方法开头间的偏移量,作用类似于 ARM 架构 CPU 的 PC 寄存器和 x86 架构 CPU 的 IP 寄存器。与这两种寄存器不同的是,PC 计数器只对当前方法有效,Java 虚拟机通过它的值来取指令并执行
  • Java 栈用于记录 Java 方法调用的活动记录(activation record),以帧(frame)为单位保存线程的运行状态:每调用一个方法,就会分配一个新的栈帧并压入 Java 栈;每从一个方法返回,则弹出并撤销相应的栈帧。每个栈帧包括局部变量区、求值栈(JVM 规范中称操作数栈)及其他信息。局部变量区存储方法的参数和局部变量,参数按照源码中从左到右的顺序保存在局部变量区开头的几个 slot 中。求值栈保存求值的中间结果及调用其他方法的参数等
  • 下图即为 JVM 运行时求值栈的状态。由于每条指令占 1 字节,foo() 函数 Java 字节码左边的偏移量即程序执行每行代码时 PC 计数器的值(Java 虚拟机最多支持 0xff 条指令)
    在这里插入图片描述
  • java 字节码具体分析
  • 第一条指令:iload_1:第一部分为 iload,这是 JVM(Java 虚拟机)指令集 load 系列指令中的一条,i 是指令前缀,表示操作类型为 int;load表示将局部变量存入 Java 栈。类似的指令有 lload、fload 等,表示使 long、float 类型的数据入栈。第二部分为数字 1,表示要操作的是哪个局部变量。索引值从 0 开始计数。如 iload_1 表示使第 2 个 int 类型的局部变量入栈,而这个局部变量即存放在局部变量区 foo() 函数中的第 2 个参数
  • 第二条指令:iload_2:取第 3 个参数
  • 第三条指令:iadd:从栈顶弹出两个 int 类型的值并求和,把结果压回栈顶
  • 第四、五条指令:再次压入第 2 和第 3 个参数
  • 第六条指令:isub:从栈顶弹出两个 int 类型的值并求差,把结果压回栈顶
  • 第七条指令:imul:从栈顶弹出两个 int 类型的值并求积,把结果压回栈顶
  • 第八条指令:ireturn:返回一个 int 类型的值
Dalvik 字节码

在这里插入图片描述

  • 第一条指令:add-int v0, v3, v4:将 v3 和 v4 寄存器的值相加,将结果存到 v0 寄存器。在整个指令操作中使用三个参数,v3 和 v4 代表 foo() 函数的第 1 和第 2 个参数
  • 第二条指令:sub-int v1, v3, v4:将 v3 和 v4 寄存器的值相减,将结果存到 v1 寄存器
  • 第三条指令:mul-int/2addr v0, v1:将 v0 和 v1 寄存器的值相乘,将结果存到 v0 寄存器
  • 第四条指令:return v0:返回 v0 寄存器的值
  • Dalvik 虚拟机运行时也为每个线程维护了一个 PC 计数器和一个调用栈
  • 与 Java 虚拟机不同的是,调用栈维护了一个寄存器列表,寄存器数量在方法结构体的 registers 字段中给出,如下图,Dalvik 虚拟机会根据这个值创建一份虚拟的寄存器列表
    在这里插入图片描述
  • 下图为 Dalvik 虚拟机运行时的状态
    在这里插入图片描述
  • 综上,可发现:与基于栈架构的 Java 虚拟机相比,基于寄存器架构的 Dalvik 虚拟机由于生成的代码更少,程序执行速度更快

虚拟机的执行流程

  • Android 源码根目录下的 Dalvik 目录存放了所有与 Dalvik 相关的代码实现和文档,可通过这些内容深入理解 Dalvik 的运行机制
  • Android 系统由 Linux 内核、函数库、Android 运行时、应用程序框架及应用程序组成,如下图
    在这里插入图片描述
  • Android 系统采用分层思想,优点:各层之间的依赖性降低、便于独立分发、容易收敛问题和错误等
  • Dalvik 虚拟机属于 Android 运行时环境,与一些核心库一起承担 Android 应用程序的运行工作
  • Android 系统启动并加载内核后,会立即执行 init 进程。init 进程先完成设备的初始化工作,再读取 init.rc 文件并启动系统中的重要外部程序 Zygote
  • Zygote 是 Android 系统中所有进程的孵化器进程。Zygote 启动后,会先初始化 Dalvik 虚拟机,再启动 system_server 进程并进入 Zygote 模式,通过 socket 等候命令下达。在执行一个 Android 应用程序时,system_server 进程通过 Binder IPC 方式将命令发送给 Zygote。Zygote 收到命令后,通过 fork 其自身创建一个 Dalvik 虚拟机的实例来执行应用程序的入口函数,从而完成程序的启动过程。如下图
    在这里插入图片描述
  • Zygote 提供了三种创建进程的方法:
  • fork():创建一个 Zygote 进程(这种方式实际不会被调用)
  • forAndSpecialize():创建一个非 Zygote 进程
  • forSystemServer():创建一个系统服务进程
  • Zygote 进程可再分成其他进程,非 Zygote 进程不能再分成其他进程。系统服务进程终止后,其子进程也必须终止
  • 进程 fork 成功后,执行工作将交给 Dalvik 虚拟机来完成
  • Dalvik 虚拟机先通过 loadClassFromDex() 函数装载类。每个类被成功解析后,都会获得运行时环境中的一个 ClassObject 类型的数据结构存储(虚拟机使用 gDvm.loadedClasses 全局散列表来存储和查询所有装载进来的类)。接着,字节码验证器使用 dvmVerifyCodeFlow() 函数对装入的代码进行校验,虚拟机调用 FindClass() 函数查找并装载 main() 方法类。最后,虚拟机调用 dvmInterpret() 函数初始化解释器并执行字节码流

虚拟机的执行方式

  • 即时编译(just-in-time Compilation, JIT):又称动态编译。一种通过在运行时将
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值