1.什么是Dalvik虚拟机Dalvik是Google为Android平台设计的Java虚拟机。它是Android平台的重要组成部分,支持Java应用程序以dex格式(DalvikExecutable)程序运行。dex格式是专门为Dalvik设计的一种压缩格式,适用于内存和处理器速度有限的系统。Google对其进行了专门的优化,使Dalvik高效、简洁、节省资源。从Android系统架构图来看,Dalvik虚拟机运行在Android运行时库层。2.Dalvik虚拟机的功能作为为Linux和嵌入式操作系统设计的虚拟机,Dalvik主要负责对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾收集。Dalvik充分利用了Linux进程管理的特殊性,对其进行了面向对象的设计,使得多个进程可以同时运行,而传统的Java程序通常只运行一个进程,这也是Android不使用的原因虚拟机。为了达到优化的目的,Dalvik的大部分底层操作都与系统内核相关,或者直接调用内核接口。另外,Dalvik早期是没有JIT编译器的,直到Android2.2才加入了对JIT的技术支持。3、Dalvik虚拟机和Java虚拟机的区别从本质上讲,Dalvik也是一个Java虚拟机。但它的特别之处在于它没有使用JVM规范。大多数Java虚拟机都是基于栈结构的(具体可以参考:理解Java虚拟机架构),而Dalvik虚拟机是基于寄存器的。基于栈的指令非常紧凑,例如Java虚拟机使用的指令只占用一个字节,所以称为字节码。基于寄存器的指令需要更多的指令空间,因为它们需要指定源地址和目标地址。Dalvik虚拟机的某些指令需要两个字节。基于堆栈和基于寄存器的指令集各有优缺点。一般来说,执行同样的功能,前者需要更多的指令(??主要是加载和存储指令),而后者需要更多的指令空间。需要更多的指令意味着更多的CPU时间,而需要更多的指令空间意味着数据缓存(d-cache)更容易失效。更多讨论,虚拟机漫谈(一):解释器,树遍历解释器,基于栈和基于寄存器,大杂烩给出了非常详细的参考。Java虚拟机运行Java字节码,而Dalvik虚拟机运行专有文件格式dex。在Java程序中,一个Java类会被编译成一个或多个class文件,然后打包成一个jar文件,然后Java虚拟机会从相应的class文件和jar文件中获取相应的字节码。Android应用程序虽然也使用Java语言,但是编译成class文件后,所有的class文件都会通过DEX工具转换成dex文件,Dalvik虚拟机从中读取指令和数据。dex文件不仅减少了整体文件大小和I/O操作次数,而且提高了类的搜索速度。从下图可以看出jar和apk文件的组成结构,以及class文件和dex文件的区别。dex格式文件使用共享的、类型特定的常量池机制来节省内存。常量池存储类中所有的字面量常量,包括字符串常量、字段常量和其他值。总的来说,Dalvik虚拟机有以下特点:使用dex格式的字节码,不兼容Java字节码格式代码密度小,运行效率高,节省资源常量池只使用32位索引,有内存限制默认堆栈大小为12KB(3页,每页4KB)。堆的默认启动大小为2MB,默认最大值为16MB。堆支持的最小启动大小为1MB,最大支持值为1024MB。heap和stack参数可以通过-Xms和-Xmx修改传递4.Dalvik体系结构Dalvik其实是基于ApacheHarmony(Apache软件基金会的JavaSE项目)的部分实现,并提供了自己的一套类库,即用于编写上层Java应用程序的API。上图来自tech-insider。ApacheHarmony大致分为三层:操作系统、Java虚拟机、Java类库。其特点是虚拟机和类库高度模块化,每个模块都有一定的接口定义。操作系统层和虚拟机层之间的接口由PortabilityLayer定义,它封装了不同操作系统的差异,为虚拟机本地代码和类库提供了一套统一的API访问底层系统调用。虚拟机与类库的接口除了Java规范定义的JNI和JVMITI外,还增加了一层虚拟机接口,由内核类和本地代码组成。实现虚拟机接口的虚拟机可以使用Harmony的类库来实现,并且可以由Harmony提供的同一个Java启动器启动。下面是Dalvik虚拟机的结构图:一个应用程序首先通过DX工具将class文件转换为Dalvik虚拟机可以执行的dex文件,然后通过class加载native类和Java类loader,然后使用解释器根据指令集解释Dalvik。字节码被解释和执行。***,根据dvm_arch参数选择编译好的目标机架构。4.1dex文件结构dex文件结构和class文件结构有很多不同,但是从携带的信息来看,dex和class文件是一致的。header:存放了每种数据类型的起始地址和偏移量等信息。proto_ids:描述函数原型信息,包括返回值和参数信息。比如"test:()V"methods_ids:函数信息,包括它所属的类和对应的proto信息。更多关于dex格式的内容,Android安全-Dex文件格式详解这篇文章介绍的很详细。虽然dex文件的结构非常紧凑,但是如果想进一步提升运行时性能,就需要进一步优化dex文件。优化主要集中在以下几个方面:调整结构中每个字段的字节顺序和所有字段的对齐方式,验证dex文件中的所有类,优化一些特定的类,优化方法中的操作码。dex文件经过优化后,文件体积会扩大,大约是原来的1~4倍。对于内置应用程序,系统编译后一般会生成一个优化文件(odex:Optimizeddex)。一个Android应用程序要在Dalvik虚拟机上运行需要经过以下过程:将Java源文件编译成class文件使用DX工具将class文件转换为dex文件使用aapt工具将dex转换文件、资源文件、AndroidManifest.xml文件(二进制格式)组合成APK安装APK到Android设备并运行上图(来源于网络)详细展示了最终签名的APK是怎么来的。4.2Dalvik类加载器一个dex文件需要一个类加载器加载本地类和Java类,然后通过解释器根据指令集解释执行Dalvik字节码。Dalvik类加载器使用mmap函数将dex文件映射到内存中,通过普通的内存读取操作访问dex文件,然后解析dex文件的内容并将其中的类加载到哈希表中。4.2.1解析dex一般来说,一个dex文件可以抽象为三个部分:header,index,data。索引的位置和数量,以及数据区的起始位置都可以通过头部获知。dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行解析,解析结果放在DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向类索引的起始位置。为了加快类的查找速度,还创建了一个哈希表,对类名进行哈希处理,生成索引。4.2.2加载类解析工作完成后,加载类,加载的类需要存放在ClassObject数据结构中。typedefstructObject{ClassObject*clazz;//类型对象Locklock;//锁定对象}Object;其中clazz指向ClassObject对象,还包含一个Lock对象。如果其他线程想要获取它的锁,就必须等待这个线程释放它。Dalvik每次加载一个类,都会对应一个ClassObject对象。加载过程会在内存中分配几个区域,分别存放directMethod、virtualMethod、sfield、ifield。这些信息都是从dex文件的数据区读取的。字段Field定义如下:structField{ClassObject*clazz;//类型constchar*name;//变量名constchar*signature;//如"Landroid/os/Debug;"u4accessFlags;//访问标志#ifdefPROFILE_FIELD_ACCESSu4gets;u4puts;#万一};得到类索引后,实际加载由loadClassFromDex完成。首先会读取类的具体数据,分别加载directMethod、virtualMethod、ifield和sfield,然后为ClassObject数据结构分配内存,读取dex文件的相关信息。加载完成后,通过dvmAddClassToHash函数将加载的类放入哈希表中,方便下次查找;***,通过dvmLinkClass找到该类的超类,如果有接口类就加载对应的接口类。4.3Dalvik解释器对于任何虚拟机来说,解释器无疑是核心部分,所有的Java字节码都是由解释器解释执行的。由于Dalvik解释器的效率非常重要,Android实现了C语言版本和各种汇编语言版本的解释器。解释器通常是循环执行的,执行第一条指令需要一个入口函数调用handler,然后每条指令执行时引出下一条指令,通过函数指针调用handler。4.4内存管理垃圾回收是Dalvik虚拟机内存管理的核心。这里只介绍Dalvik虚拟机的垃圾回收功能。垃圾回收的性能极大地影响Java程序的内存使用效率。Dalvik虚拟机采用了常用的Mark-Sweep算法,分为Mark阶段(标记活动对象)、Sweep阶段(回收垃圾内存)和可选的Compact阶段(减少堆中的碎片)。Android内存管理原理这篇文章详细讲解。垃圾回收的第一步是标记存活对象,因为没有办法识别那些不可访问的对象,所以所有未标记的对象都是可以回收的垃圾。当进行垃圾回收时,需要停止Dalvik虚拟机的运行(垃圾回收除外),因此垃圾回收又称为STW(stop-the-world)。Dalvik虚拟机在运行过程中需要维护一些状态信息,包括:每个线程保存的寄存器、Java类中的静态字段、本地和全局的JNI引用,JVM中所有的函数调用都会对应一个对应的C栈帧。每个堆栈帧都可能包含对对象的引用,例如包含对象引用的局部变量和参数。所有这些引用信息都被添加到一个根集合中,然后从根集合开始,递归地寻找可以从根集合访问的对象。因此,Mark过程也称为跟踪,跟踪所有可访问的对象。垃圾回收的第二步是回收内存。在Mark阶段,可以通过markBits位图获取所有可访问的对象集合,liveBits位图代表所有已分配的对象集合。通过比较liveBits位图和markBits位图的区别就是所有可回收对象的集合。Sweep阶段调用free将内存释放到堆中。在内存底层实现上,Android系统使用的是msspace,是一个轻量级的malloc实现。除了创建和初始化用于存储普通Java对象的内存堆外,Android还创建了三个额外的内存堆:“livebits”(用于存储堆上占用内存的位图索引)“markbits”(GC时使用Bitmap用于标记幸存对象的index)"markstack"(遍历GC中幸存对象引用的标记栈)虚拟机通过一个名为gHs的全局HeapSource变量控制GC内存堆,HeapSource中的heaps数组可以管理多个A堆(Heap)以满足动态调整GC内存堆大小的要求。此外,HeapSource还维护了一个名为“livebits”的位图索引来跟踪每个堆(Heap)的内存使用情况。剩下的两个数据结构“markstack”和“markbits”用于垃圾回收阶段。上图中,“livebits”在堆上维护着已使用的内存信息,位图索引“markbits”指向存活的对象。对象A、C、F、G、H需要保留,所以“markbits”分别指向它们(***的H对象还在标记过程中,所以没有指向它的指针)。而“markstack”是在标注过程中用来跟踪当前需要处理的对象的标记栈。此时,它保存了正在处理的对象F、G、H。4.5Dalvik的启动过程Dalvik的进程管理依赖于linux的进程架构。如果要为一个应用程序创建一个进程,它会使用linux的fork机制来复制一个进程。Zygote是一个虚拟机进程,是一个虚拟机实例的孵化器,由init进程启动。上一篇文章对此过程有详细介绍:Android系统启动分析(Init->Zygote->SystemServer->Homeactivity)。启动Dalvik虚拟机的相关过程就分析到这里。AndroidRuntime类主要做了以下几件事:调用startVM创建Dalvik虚拟机,JNI_CreateJavaVM实际创建并初始化一个虚拟机实例调用startReg注册Android核心类的JNI方法通过Zygote进程进入Java层在JNI中,dvmCreateJNIEnv为当前线程创建并初始化一个JNI环境,即一个JNIEnvExt对象。***调用dvmStartup初始化之前创建的Dalvik虚拟机实例。函数dvmInitZygote调用系统的setpgid设置当前进程,即Zygote进程的进程组ID。这一步完成后,Dalvik虚拟机的创建和初始化就完成了。5、启动Android,启动电源,加载bootloader到RAMBootLoader引导LinuxKernel,启动Init进程,创建InitforkoutofZygote进程,Zygote进程创建虚拟机;创建系统服务AndroidHomeLauncher启动参考:《Android技术内幕》Dalvik虚拟机简介及学习计划(http://blog.csdn.net/luoshengyang/article/details/8852432)深入理解Android(二):Java虚拟机Dalvik(http://www.infoq.com/cn/articles/android-in-depth-dalvik)dalvik虚拟内存管理二-垃圾回收(http://www.miui.com/thread-75028-1-1.html)Dalvik虚拟机启动过程分析(http://blog.csdn.net/luoshengyang/article/details/8885792)
