1. Dalvik 虚拟机
1.1 Dalvik 虚拟机概述
Dalvik 虚拟机 (Dalvik Virtual Machine ), 简称 Dalvik VM 或者 DVM。DVM 是 Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。每一个应用程序对应有一个单独的Dalvik虚拟机实例。
DVM 作为 Android 平台至关重要的中间件,它的输入是经过dx工具 打包好的Dex文件 ,输出是程序执行结果。dx 工具解析 .class文件,合并多个 .class文件,转换为基于寄存器的字节码,并优化字节码,最终生成 Dex文件。
DVM 在 API 上和 Java API 是兼容的。编写好的Java程序可以直接用PC上的Java编译器编译为 .class 文件,之后,需要使用 Dalvik VM提供的dx工具对其进行转换。生成的 Dex文件将作为 DVM 的输入。DVM 启动并初始化后,Dex文件将被映射到内存区,解释器开始将 Dex文件中的每一条字节码解释为本地代码并运行。解释器的工作流程和真实CPU 的工作原理非常相像,都包括取指、解码以及执行。具体的实现原理较为简单,以一个循环来完成一条字节码的解释工作。Dalvik 解释器从内存中取得字节码,并对字节码进行解码(获取字节码号),之后跳转到对应的代码段执行。无论是C语言编写的解释器还是汇编语言编写的解释器,每一条字节码都有一段与之等价的最终执行。如果有JIT的支持,JIT将编译热代码,并将编译后的 Native Code 安装至内存区。再次执行时,解释器将跳转至相应Native Code 执行,这将大幅度提高执行速度。
所有 Android应用程序都运行在 DVM之上,如果 Android 应用程序需要调用核心库中的库函数,DVM 将调用本地接口(Native Interface)并执行核心库中的函数。在 DVM 之上运行的程序或应用是跨平台的,但 DVM 是和操作系统及硬件相关的。其依赖操作系统掌管的一些功能,如线程调度、内存管理等。和操作系统与底层硬件相关一样,DVM 的解释器和JIT都因硬件的不同而有不同的实现,如ARM系列、MIPS以及Intel X86等平台都对应有不同的实现。
1.2 DVM 与 JVM 的区别:
DVM 并不是按照 JVM 的规范进行实现的,Dalvik 虚拟机与Java 虚拟机的类文件格式和执行的指令集是不一样的。
1.2.1 .class 文件和 .dex 文件
Dalvik 虚拟机使用的是 .dex(Dalvik Executable)格式的类文件,而 Java 虚拟机使用的是 .class 格式的类文件。
一个 .dex 文件可以包含若干个类,而一个 .clas s文件只包括一个类。
由于一个dex文件可以包含若干个类,因此它就可以将各个类中重复的字符串和其它常数只保存一次,从而节省了空间。一般来说,包含有相同类的未压缩dex文件稍小于一个已经压缩的jar文件。
1.2.2 JVM是基于栈的虚拟机:
基于栈的虚拟机在复制数据时而使用的大量的出入栈指令,需要更多的指令,多占用CPU时间。但同时指令更紧凑、更简洁。
1.2.3DVM是基于寄存器的虚拟机:
寄存器是CPU的组成部分,它们可用来暂存指令、数据等。
基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器,其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。
与 JVM 相似,在 DVM 中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。由于显示指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,需要更多的指令空间,数据缓冲更易失效。但是由于指令数量的减少,总的代码数不会增加多少。
1.3 Dalvik虚拟机的特性
DVM 非常适合在移动终端上使用,相对于在桌面系统和服务器系统运行的虚拟机而言,它不需要很快的CPU速度和大量的内存空间。
Android 是由Google公司基于移动设备而开发的嵌入式系统,具有优良的性能表现以及较低的硬件配置需求,因此使其迅速成为目前移动终端之上的主流操作系统。这种优势的体现主要得益于Google对作为Android系统基石的 DVM 所做出的大量优化。实际上,DVM 并不是一个标准的Java虚拟机,因为它不符合Java虚拟机设计规范。DVM 是一个针对嵌入式系统中低速 CPU和内存受限等特点,经过专门设计优化而实现的 Java语言虚拟机。
1.4 Dalvik 虚拟机的功能
DVM 主要完成对象生命周期的管理、堆栈的管理、线程管理、安全和异常的管理以及垃圾回收等重要功能。
在 DVM 设计的过程中充分利用了Linux进程管理的特点,使其可以同时运行多个进程,这就使得在 Android系统上可以同时运行多个应用程序,每一个应用程序都对应后台一个独立的虚拟机进程。DVM 考虑到运行环境资源相对紧张的特点,对线程管理、类加载、内存管理、本地接口、反射机制、解释器、即时编译等主要功能模块做了相应的优化及创新。
具体功能如下:
-
进程管理
:进程隔离和线程管理,每一个 Android应用在底层都会对应一个独立的Dalvik 虚拟机实例,所有的Android 应用的线程都对应一个 Linux线程,进程管理依赖于Zygote机制实现。 -
Zygote 线程管理
:每一个 Android应用都运行在一个 Dalvik 虚拟机实例里,而每一个虚拟机实例都是一个独立的进程空间 ,这样做的优点是最大程度地保护了应用的安全和独立运行 。Zygote 进程是在系统启动时产生的,它会完成虚拟机的初始化、库的加载、预置类库的加载和初始化等操作,而在系统需要一个新的虚拟机实例时,Zygote通过复制自身,最快速地提供一个虚拟机实例。另外,对于一些只读的系统库,所有虚拟机实例都和Zygote共享一块内存区域,极大地节省了内存开销。 -
类加载
:解析 Dex文件并加载 Dalvik 字节码。对Android 源码经过 Android SDK 编译后生成的 APK文件进行一系列的处理,再在展开的APK文件中找到 classes.dex文件并从 Dex文件中加载 Dalvik 字节码供虚拟机执行模块调用。类加载器在虚拟机中负责查找并加载字节码文件,即通过提取二进制的字节码文件,并将其存入该类的运行时数据结构,供解释器执行。在加载目标类时,还需要将该类的所有超类和超类实现的接口,需要加载的类分为基础类库和用户自定义类。当虚拟机装载某个类型时,类加载器会定位相应的字节码文件,然后读入这个字节码文件,提取其中的数据信息,并将这些信息存储到对应的内存中。
Android系统启动时,类加载器会加载所有基础类库,用户自定义类是在虚拟机运行时才载入的。当虚拟机在运行时需要调用的一个成员方法或者一个成员变量所属的类没有被解析的时候,虚拟机会调用类加载模块,对这个类、超类以及这些类的相关接口进行加载和连接。
-
内存管理
:分配系统启动初始化和应用程序运行时需要的内存资源。DVM 内存管理分为内存分配 和垃圾回收 。内存分配的底层依赖是基于 Doug Lea 编写的 dlmalloc 内存分配器,在 Heap上完成,按照分配规则,每分配一个内存区域经过数次尝试。如果分配不成功,就启动垃圾收集按照相应策略进行垃圾收集。DVM 在垃圾回收时使用 Mark-Sweep 算法。 -
本地接口
:让既有代码继续发挥作用。在 Java代码中调用其他代码的接口。JNI 是Java Native Interface的缩写,即Java本地调用。从Java1.1开始,JNI 标准成为 Java平台的一部分,它允许 Java 代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍使用 其他语言,只要调用约定受支持就可以了。Android系统的 DVM 实现了这套接口,供 DVM 的Java应用与本地代码实现互相调用。 -
注重处理速度
:和本地代码(C/C++等)相比,Java代码的执行速度相对慢一些。如果对某段程序的执行速度有较高的要求,建议使用C/C++编写代码。而后在Java中通过JNI调用基于C/C++编写的部分,常常能够获得更快的运行速度。例如,图形处理等需要大量计算的情况下通常会采用该机制。 -
直接进行硬件控制
:为了更好地控制硬件,硬件控制代码通常使用C语言编写。而后借助JNI将其与Java层连接起来,从而实现对硬件的控制。Dalvik 虚拟机使用一些本地代码编写的已编译的代码库与硬件、操作系统直接进行交互。 -
对既有本地代码的复用
:在程序编写过程中,常常会使用一些已经编写好的本地代码(如 C/C++代码),既提高了编程效率,又确保了程序的安全性与健壮性。在复用这些本地代码时,就要通过JNI本地调用接口来实现。从整体来看,使用JNI主要在于可以直接重用一些数量庞大的本地代码,对于那些性能要求比较高的代码而言,通过JNI机制使用本地代码可以增强程序的性能,减少功耗,特别是对于内存较小、CPU 运算速度有限和电池电量不够充沛的嵌入式移动手机设备而言,提高性能减少功耗这一点就显得尤为重要。然而,使用JNI调用接口也会带来一些负面影响,比如有些时候失去了平台的可移植性,但是从综合角度考虑,在有些情况下,JNI机制带来的优点要大于它的缺点。 -
反射机制
:能动态查看、调用、更改任意类中的方法和属性,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。反射机制是 DVM 中的核心机制之一,也算作一类工具,合理地使用反射机制能使Java代码变得更加简洁、灵活。同时,反射机制是 Java被当作准动态语言的一个关键性质。反射机制允许程序在运行的过程中通过反射机制的 API取得任何一个已知名称的类的内部信息,包括其中的描述符、超类,也包括属性和方法等所有信息,并且可以在程序运行时改变属性的相关内容或调用其内部的方法。反射机制在实现其功能时首先通过上层应用API运用JNI本地调用机制调用本地方法集中的函数,再向下层调用 DVM 中的内部函数,最后将结果逐层返回到最上层的应用。 -
解释器
:根据自身的指令集 Dalvik ByteCode 解释字节码。解释器是 Dalvik 虚拟机的执行引擎,它负责解释执行 Dex字节码。在 Android 4.04版本的虚拟机中,解释器共有两种实现,分别是C语言实现和汇编语言的实现,分别称作可移植型(Portable)解释器 和快速型(Fast)解释器 。在字节码加载已经完毕后,DVM 解释器开始取指解释字节码。解释器根据指令进行相应的操作,然后返回相应的结果。执行引擎 是虚拟机的核心,负责执行字节码或者本地方法。DVM 主要采用解释执行 的方式,Android4.04版本的虚拟机同时支持JIT编译器技术。解释执行方式是指虚拟机在执行过程中将每一条字节码指令解释成本地代码运行,其工作原理比较简单,字节码的解释过程是一个循环结构,每次循环完成一条字节码的执行工作:取指令、执行功能和跳转。简言之,解释执行方式就是把字节码指令的功能用特定平台上的语言来实现。解释执行是利用该平台的资源,不需要进行附加的硬件设计,利用软件来实现虚拟机的方式。 -
即时编译
:将反复执行的热代码编译成本地码,降低解释器压力。JIT 技术是将字节码编译成本地代码执行,当某一个方法第一次被调用时,JIT 编译器将对虚拟机方法表所指向的字节码进行编译,编译后表中的指针将指向编译生成的机器码,如果程序再次执行该方法时,将执行经过编译的代码,提高了执行速度。JIT(Just-In-Time),中文含义为即时编译,又称为动态编译,执行时动态地编译程序,以缓解解释器的低效工作。JIT混合了两种技术,解释器解释时,编译部分程序,并在下次直接执行该编译后的源程序。对于Java这类语言来说,不论是解释器还是JIT模块,都是在中间代码基础上做文章。虚拟机中最主要的是解释器,负责解释执行中间代码。虽有跨平台的优势,但同时也带来了运行效率低的直观感受。究其原因,是因为其执行原理是一句一句翻译字节码。比如,字节码中出现循环,根据解释器的原理,它需要重复地解释并执行这一组程序。这使得纯粹基于解释器运行的程序效率十分低下,造成了浪费。JIT是解决这个缺陷的一种有效手段。通过将字节码编译为Native Code,让解释器不再重复执行这些热点代码片段。而且,相比于解释器,JIT编译器可以更高效地利用CPU和寄存器。同时在编译的过程中,可以进行部分低级代码优化,比如常数传播、取消范围检查、复制传播等。尽可能地生成媲美编译器编译的二进制代码。之后执行编译生成的Native Code,从而达到加速执行应用程序的目的。
2. ART虚拟机
ART(Android Runtime)虚拟机是 Android4.4 发布的,用来替换 Dalvik 虚拟机,Android4.4 默认采用的还是DVM,系统会提供一个选项来开启ART。在 Android 5.0版本中默认采用了ART,DVM从此退出历史舞台。
2.1 ART 与 DVM 的区别
1. 编译方式
DVM 中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART 中,ART虚拟机执行的是本地机器码。
系统在安装应用程序时 会进行一次 AOT(ahead of time compilation, 预先编译) ,在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex 中的字节码将被编译成本地机器码,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电量也会降低。
采用 AOT 也会有缺点,主要有两个:
- AOT 会使得应用程序的安装时间变长,尤其是一些复杂的应用。
- 字节码预先编译成机器码,机器码需要的存储空间会多一些。
为了解决上面的缺点, Android7.0版本中的ART 加入了即时编译器 JIT, 作为AOT 的一个补充:
- 最初安装应用时不进行任何 AOT 编译(安装又快了),运行过程中解释执行,对经常执行的方法进行JIT,经过 JIT 编译的方法将会记录到Profile配置文件中。
- 当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行 AOT 编译。待下次运行时直接使用。
2. 空间划分
ART在运行时堆空间划分上与DVM不同。
3. 性能优化
ART在运行时对垃圾回收机制进行了改进,例如更频繁地执行并垃圾收集,将GC暂停的次数由2次减少到1次。
4. 支持架构
DVM是为32位CPU设计的,而ART支持64位并兼容32位的CPU。