深入理解Android虚拟机体系结构

ijtd5456 8年前
   <h3><strong>1.什么是Dalvik虚拟机</strong></h3>    <p>Dalvik虚拟机是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,使得Dalvik具有高效、简洁和节省资源的特点。从Android系统架构图可知,Dalvik虚拟机运行在Android的运行时库层</p>    <h3><strong>2.Dalvik虚拟机的功能</strong></h3>    <p>Dalvik作为面向Linux、为嵌入式操作系统涉及的虚拟机,主要负责完成对象生命周期管理、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。Dalvik充分利用Linux进程管理的特点,对其进行了面向对象设计,使得可以同时运行多个进程,而传统的Java程序通常只能运行一个进程,这也是为什么Android不采用JVM的原因。Dalvik为了达到优化的目的,底层的操作大多和系统内核相关,或者直接调用内核接口。另外,Dalvik早期并没有JIT编译器,直到Android2.2才加入了对JIT的技术支持。</p>    <h3><strong>3.Dalvik虚拟机和Java虚拟机的区别</strong></h3>    <p>本质上,Dalvik也是一个Java虚拟机。但它特别之处在于没有使用JVM规范。 大多数Java虚拟机都是基于栈的结构,而Dalvik虚拟机则是基于寄存器。 基于栈的指令很紧凑,例如, Java虚拟机使用的指令只占一个字节,因而称为字节码。基于寄存器的指令由于需要指定源地址和目标地址,因此需要占用更多的指令空间。 Dalvik虚拟机的某些指令需要占用两个字节。基于栈和基于寄存器的指令集各有优劣,一般而言,执行同样的功能,前者需要更多的指令(主要是load和store指令),而后者需要更多的指令空间。需要更多指令意味着多占用CPU时间,而需要更多指令空间意味着数据缓冲(d-cache)更易失效。</p>    <p>Java虚拟机运行的是Java字节码,而Dalvik虚拟机运行的是专有文件格式dex。在Java程序中,Java类会被编译成一个或者多个class文件,然后打包到jar文件中,接着Java虚拟机会从相应的class文件和jar文件中获取对应的字节码。Android应用虽然也使用Java语言,但是在编译成class文件后,还会通过DEX工具将所有的class文件转换成一个dex文件,Dalvik虚拟机再从读取指令和数据。 dex文件除了减少整体的文件尺寸和I/O操作次数,也提高了类的查找速度。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0fc034309914662dece1173c21c30a72.png"></p>    <p>由上图可以看到,jar和apk文件的组成结构,以及class文件和dex文件的差异。dex格式文件使用共享的、特定类型的常量池机制类节省内存。常量池存储类中的所有字面常量,它包括字符串常量、字段常量等值。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bd59bf1e0a12b2281de03d7674bee55f.jpg"></p>    <p>总的来说,Dalvik虚拟机具有以下的特点:</p>    <ul>     <li> <p>使用dex格式的字节码,不兼容Java字节码格式</p> </li>     <li> <p>代码密度小,运行效率高,节省资源</p> </li>     <li> <p>常量池只使用32位的索引</p> </li>     <li> <p>有内存限制</p> </li>     <li> <p>默认栈大小是12KB(3个页,每页4KB)</p> </li>     <li> <p>堆默认启动大小为2MB,默认最大值为16MB</p> </li>     <li> <p>堆支持的最小启动大小为1MB,支持的最大值为1024MB</p> </li>     <li> <p>堆和栈参数可以通过-Xms和-Xmx修改</p> </li>    </ul>    <h3><strong>4.Dalvik系统结构</strong></h3>    <p>实际上,Dalvik是基于Apache Harmony(Apache软件基金会的Java SE项目)的部分实现,提供了自己的一套库,即上层Java应用程序编写所使用的API。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d79a2dd930ce8820b615a20f636c19d1.jpg"></p>    <p>以上图示来自tech-insider。Apache Harmony大体上分为三层:操作系统、Java虚拟机和Java类库。它的特定在于虚拟机和类库内部被高度模块化,每一个模块都有一定的接口定义。操作系统层和虚拟机层之间的接口由Portability Layer定义,它封装了不同的操作系统的差异,为虚拟机和类库的本地代码提供了一套统一的API访问底层系统调用。虚拟机和类库之间的接口除了Java规范定义的JNI、JVMITI外,还加入了一层虚拟机接口,由内核类和本地代码组成。实现了虚拟机接口的虚拟机都可以使用Harmony的类库实现,并且可以被Harmony提供的同一个Java启动程序启动。</p>    <p><strong>下面是Dalvik虚拟机的结构图:</strong></p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/ab10e509e669a539c86df89d8c7e8979.jpg"></p>    <p>一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。</p>    <p><strong>4.1 dex文件结构</strong></p>    <p>dex文件结构和class文件结构差异的地方很多,但从携带的信息上看,dex和class文件是一直的。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/b029bbc60b3b8a11edf1da5e15ca530d.jpg"></p>    <ul>     <li> <p>header:存储了各个数据类型的起始地址、偏移量等信息。</p> </li>     <li> <p>proto_ids:描述函数原型信息,包括返回值,参数信息。比如“test:()V”</p> </li>     <li> <p>methods_ids:函数信息,包括所属类及对应的proto信息。</p> </li>    </ul>    <p>虽然dex文件的结构很紧凑,但想要运行时的性能得到进一步提升,还需要对dex文件进行进一步优化。优化主要针对以下几个方面:</p>    <ul>     <li> <p>调整所有字段的字节序和对齐结构中的每一个域</p> </li>     <li> <p>验证dex文件中的所有类</p> </li>     <li> <p>对一些特定的类进行优化,对方法里的操作码进行优化</p> </li>    </ul>    <p>dex文件经过优化后文件大小会膨胀,大约增加到原来的1~4倍。对于内置应用,一般在系统编译后,便会生成优化文件(odex:Optimized dex)。 一个Android引用程序,需要经过以下过程才可以在Dalvik虚拟机上运行:</p>    <ul>     <li> <p>把Java源文件编译成class文件</p> </li>     <li> <p>使用DX工具把class文件转换成dex文件</p> </li>     <li> <p>使用aapt工具把dex文件、资源文件以及AndroidManifest.xml文件(二进制格式)组合成APK</p> </li>     <li> <p>将APK安装到Android设备运行</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0decef151514cecab17d5da2fae1ea20.jpg"></p>    <p>上图(来自网络)详尽地展示了最终签名后的APK是怎么来的。</p>    <p><strong>4.2 Dalvik类加载器</strong></p>    <p>一个dex文件需要类加载器加载原生类和Java类,然后通过解释器根据指令集对Dalvik字节码进行解释和执行。Dalvik类加载器使用mmap函数,将dex文件映射到内存中,通过普通的内存读取操作即可访问dex文件,然后解析dex文件内容并加载其中的类到哈希表中。</p>    <p><strong>4.2.1 解析dex</strong></p>    <p>总的来说,dex文件可以抽象为三个部分:头部、索引和数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行分析,分析的结果放到DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向class索引的起始位置。为了加快class的查找速度,还创建一个哈希表,对class名字进行了哈希并生成索引。</p>    <p><strong>4.2.2 加载class</strong></p>    <p>解析工作完成后就进行class的加载,加载的类需要用ClassObject数据结构来存储。</p>    <p>typedef struct Object {</p>    <p>ClassObject* clazz; //类型对象</p>    <p>Lock lock; //锁对象</p>    <p>} Object</p>    <p>其中clazz指向ClassObject对象,还包含一个Lock对象。如果其它线程想要获取它的锁,只有等这个线程释放。Dalvik每加载一个class都会对应一个ClassObject对象,加载过程会在内存中分配几个区域,分别存放directMethod,virtualMethod、sfiled和ifiled。这些信息从dex文件的数据区中读取。</p>    <p>得到class索引后,实际的加载由loadClassFromDex来完成。首先会读取class的具体数据,分别加载directMethod,virtualMethod、ifield和sfield,然后为ClassObject数据结构分配内存,并读取dex文件的相关信息。加载完成后,将加载的class通过dvmAddClassToHash函数放入哈希表,以方便下次查找;最后,通过dvmLinkClass查找该类的超类,如果有接口类则加载相应的接口类。</p>    <p><strong>4.3 Dalvik解释器</strong></p>    <p>对于任何虚拟机来说,解释器无疑是核心的部分,所有的Java字节码都经过解释器解释执行。 由于Dalvik解释器的效率很重要,Android分别实现了C语言版和各种汇编语言版的解释器。 解释器通常是循环执行,需要一个入口函数调用处理程序执行第一条指令,而后每条指令执行时引出下一条指令,通过函数指针调用处理程序。</p>    <p><strong>4.4 内存管理</strong></p>    <p>垃圾收集是Dalvik虚拟机内存管理的核心。此处只接受Dalvik虚拟机的垃圾收集功能。垃圾收集的性能在很大程度上影响一个Java程序内存使用的效率。Dalvik虚拟机使用常用的Mark-Sweep算法,该算法分Mark阶段(标记出活动对象)、Sweep阶段(回收垃圾内存)和可选的Compact阶段(减少堆中的碎片)。</p>    <p>垃圾收集的第一步是标记出活动对象,因为没有办法识别那些不可访问的对象,这样所有未被标记的对象就是可以回收的垃圾。当进行垃圾收集时,需要停止Dalvik虚拟机的运行(除垃圾收集外),因此垃圾收集又被称作STW(stop-the-world)。Dalvik虚拟机在运行过程中要维护一些状态信息,这些信息包括:每个线程所保存的寄存器、Java类中的静态字段、局部和全局的JNI引用,JVM中的所有函数调用会对应一个相应C的栈帧。每一个栈帧里可能包含对对象的引用,比如包含对象引用的局部变量和参数。所有这些引用信息被加入到一个根集合中,然后从根集合开始,递归查找可以从根集合出发访问的对象。因此,Mark过程又叫做追踪,追踪所有可被访问的对象。</p>    <p>垃圾收集的第二步就是回收内存。在Mark阶段通过markBits位图可以得到所有可访问的对象集合,而liveBits位图表示所有已经分配的对象集合。通过比较liveBits位图和markBits位图的差异就是所有可回收的对象集合。Sweep阶段调用free来释放这些内存给堆。</p>    <p>在底层内存实现上,Android系统使用的是msspace,这是一个轻量级的malloc实现。除了创建和初始化用于存储普通Java对象的内存堆,Android还创建三个额外的内存堆:</p>    <ul>     <li> <p>"livebits"(用来存放堆上内存被占用情况的位图索引)</p> </li>     <li> <p>"markbits"(在GC时用于标注存活对象的位图索引)</p> </li>     <li> <p>“markstack”(在GC中遍历存活对象引用的标注栈)</p> </li>    </ul>    <p>虚拟机通过一个名为gHs的全局HeapSource变量来操控GC内存堆,而HeapSource里通过heaps数组可以管理多个堆(Heap),以满足动态调整GC内存堆大小的要求。另外HeapSource里还维护一个名为"livebits"的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构"markstack"和"markbits"都是用在垃圾回收阶段。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/8bf5a3aa2b3c21d2630d3b6ca33ed82f.png"></p>    <p>上图中"livebits"维护堆上已用的内存信息,而"markbits"这个位图索引则指向存活的对象。 A、C、F、G、H对象需要保留,因此"markbits"分别指向他们(最后的H对象尚在标注过程中,因此没有指针指向它)。而"markstack"就是在标注过程中跟踪当前需要处理的对象要用到的标志栈,此时其保存了正在处理的对象F、G和H。</p>    <p><strong>4.5 Dalvik的启动流程</strong></p>    <p>Dalvik进程管理是依赖于linux的进程体系结构的,如要为应用程序创建一个进程,它会使用linux的fork机制来复制一个进程。Zygote是一个虚拟机进程,同时也是一个虚拟机实例的孵化器,它通过init进程启动。之前的文章有对此过程有详细介绍:Android系统启动分析(Init->Zygote->SystemServer->Home activity)。此处分析Dalvik虚拟机启动的相关过程。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d1f455fe1a75dde51824f149902552a4.png"></p>    <p>AndroidRuntime类主要做了以下几件事情:</p>    <ul>     <li> <p>调用startVM创建一个Dalvik虚拟机,JNI_CreateJavaVM真正创建并初始化虚拟机实例</p> </li>     <li> <p>调用startReg注册Android核心类的JNI方法</p> </li>     <li> <p>通过Zygote进程进入Java层</p> </li>    </ul>    <p>在JNI中,dvmCreateJNIEnv为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象。最后调用dvmStartup来初始化前面创建的Dalvik虚拟机实例。函数dvmInitZygote调用了系统的setpgid来设置当前进程,即Zygote进程的进程组ID。这一步完成后,Dalvik虚拟机的创建和初始化工作就完成了。</p>    <h3><strong>5 Android的启动</strong></h3>    <ul>     <li> <p>启动电源,加载引导程序到RAM</p> </li>     <li> <p>BootLoader引导</p> </li>     <li> <p>Linux Kernel启动</p> </li>     <li> <p>Init进程创建</p> </li>     <li> <p>Init fork出Zygote进程,Zygote进程创建虚拟机;创建系统服务</p> </li>     <li> <p>Android Home Launcher启动</p> </li>    </ul>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/21b13c1a24e75c3ad7b6d726fa04101d.jpg"></p>    <p><strong>参考:</strong></p>    <p>《Android技术内幕》</p>    <p>Dalvik虚拟机简要介绍和学习计划</p>    <p>深入理解Android(二):Java虚拟机Dalvik</p>    <p>dalvik虚拟内存管理之二——垃圾收集</p>    <p>Dalvik虚拟机的启动过程分析</p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzI0MjE3OTYwMg==&mid=2649548378&idx=1&sn=8bb7e76b3cb0733f4e88059447d02c2f&chksm=f1180d27c66f843107ae25864f4de466fbdc4f23e60b9b0eead7b3080cf2009ff130563df997#rd</p>    <p> </p>