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):又称动态编译。一种通过在运行时将