Dalvik虚拟机简要介绍和学习计划

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8852432html

咱们知道,Android应用程序是运行在Dalvik虚拟机里面的,而且每个应用程序对应有一个单独的Dalvik虚拟机实例。除了指令集和类文件格 式不一样,Dalvik虚拟机与Java虚拟机共享有差很少的特性,例如,它们都是解释执行,而且支持即时编译(JIT)、垃圾收集(GC)、Java本地 方法调用(JNI)和Java远程调试协议(JDWP)等。本文对Dalvik虚拟机进行简要介绍,以及制定学习计划。java

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!android

        Dalvik虚拟机是由Dan Bornstein开发的,名字来源于他的祖先曾经居住过的位于冰岛的同名小渔村。Dalvik虚拟机起源于Apache Harmony项目,后者是由Apache软件基金会主导的,目标是实现一个独立的、兼容JDK 5的虚拟机,并根据Apache License v2发布。因而可知,Dalvik虚拟机从诞生的那一天开始,就和Java有说不清理不断的关系。多线程

        Dalvik虚拟机与Java虚拟机的最显著区别是它们分别具备不一样的类文件格式以及指令集。Dalvik虚拟机使用的是dex(Dalvik Executable)格式的类文件,而Java虚拟机使用的是class格式的类文件。一个dex文件能够包含若干个类,而一个class文件只包括一 个类。因为一个dex文件能够包含若干个类,所以它就能够将各个类中重复的字符串和其它常数只保存一次,从而节省了空间,这样就适合在内存和处理器速度有 限的手机系统中使用。通常来讲,包含有相同类的未压缩dex文件稍小于一个已经压缩的jar文件。并发

        Dalvik虚拟机使用的指令是基于寄存器的,而Java虚拟机使用的指令集是基于堆栈的。基于堆栈的指令很紧凑,例如,Java虚拟机使用的指令只占一 个字节,于是称为字节码。基于寄存器的指令因为须要指定源地址和目标地址,所以须要占用更多的指令空间,例如,Dalvik虚拟机的某些指令须要占用两个 字节。基于堆栈和基于寄存器的指令集各有优劣,通常而言,执行一样的功能,前者须要更多的指令(主要是load和store指令),然后者须要更多的指令 空间。须要更多指令意味着要多占用CPU时间,而须要更多指令空间意味着数据缓冲(d-cache)更易失效。oracle

        此外,还有一种观点认为,基于堆栈的指令更具可移植性,由于它不对目标机器的寄存器进行任何假设。然而,基于寄存器的指令因为对目标机器的寄存器进行了假设,所以,它更有利于进行AOT(ahead- of-time)优化。 所谓AOT,就是在解释语言程序运行以前,就先将它编译成本地机器语言程序。AOT本质上是一种静态编译,它是是相对于JIT而言的,也就是说,前者是在 程序运行前进行编译,然后者是在程序运行时进行编译。运行时编译意味着能够利用运行时信息来获得比较静态编译更优化的代码,同时也意味不能进行某些高级优 化,由于优化过程太耗时了。另外一方面,运行前编译因为不占用程序运行时间,所以,它就能够不计时间成原本优化代码。不管AOT,仍是JIT,最终的目标都 是将解释语言编译为本地机器语言,而本地机器语言都是基于寄存器来执行的,所以,在某种程度来说,基于寄存器的指令更有利于进行AOT编译以及优化。app

        事实上,基于寄存器和基于堆栈的指令集之争,就如精简指令集(RISC)和复杂指令集(CISC)之争,谁优谁劣,至今是没有定论的。例如,上面提到完成 相同的功能,基于堆栈的Java虚拟机须要更多的指令,所以就会比基于寄存器的Dalvik虚拟机慢,然而,在2010年,Oracle在一个ARM设备 上使用一个non-graphical Java benchmarks来对比Java SE Embedded和Android 2.2的性能,发现后者比前者慢了2~3倍。上述性能比较结论以及数据能够参考如下两篇文章:框架

        1. Virtual Machine Showdown: Stack Versus Registerside

        2. Java SE Embedded Performance Versus Android 2.2函数

        基于寄存器的Dalvik虚拟机和基于堆栈的Java虚拟机的更多比较和分析,还能够参考如下文章:

        1. http://en.wikipedia.org/wiki/Dalvik_(software)

        2. http://www.infoq.com/news/2007/11/dalvik

        3. http://www.zhihu.com/question/20207106

        无论结论如何,Dalvik虚拟机都在尽最大的努力来优化自身,这些措施包括:

        1. 将多个类文件收集到同一个dex文件中,以便节省空间;

        2. 使用只读的内存映射方式加载dex文件,以即可以多进程共享dex文件,节省程序加载时间;

        3. 提早调整好字节序(byte order)和字对齐(word alignment)方式,使得它们更适合于本地机器,以便提升指令执行速度;

        4. 尽可能提早进行字节码验证(bytecode verification),提升程序的加载速度;

        5. 须要重写字节码的优化要提早进行。

        这些优化措施的更具体描述能够参考Dalvik Optimization and Verification With dexopt一文。

        分析完Dalvik虚拟机和Java虚拟机的区别以后,接下来咱们再简要分析一下Dalvik虚拟机的其它特性,包括内存管理、垃圾收集、JIT、JNI以及进程和线程管理。

        一. 内存管理

        Dalvik虚拟机的内存大致上能够分为Java Object Heap、Bitmap Memory和Native Heap三种。

        Java Object Heap是用来分配Java对象的,也就是咱们在代码new出来的对象都是位于Java Object Heap上的。Dalvik虚拟机在启动的时候,能够经过-Xms和-Xmx选项来指定Java Object Heap的最小值和最大值。为了不Dalvik虚拟机在运行的过程当中对Java Object Heap的大小进行调整而影响性能,咱们能够经过-Xms和-Xmx选项来将它的最小值和最大值设置为相等。

        Java Object Heap的最小和最大默认值为2M和16M,可是手机在出厂时,厂商会根据手机的配置状况来对其进行调整,例如,G一、Droid、Nexus One和Xoom的Java Object Heap的最大值分别为16M、24M、32M 和48M。咱们能够经过ActivityManager类的成员函数getMemoryClass来得到Dalvik虚拟机的Java Object Heap的最大值。

        ActivityManager类的成员函数getMemoryClass的实现以下所示:

[java] view plain copy 在CODE上查看代码片 派生到个人代码片
  1. public class ActivityManager {  
  2.     ......  
  3.   
  4.     /** 
  5.      * Return the approximate per-application memory class of the current 
  6.      * device.  This gives you an idea of how hard a memory limit you should 
  7.      * impose on your application to let the overall system work best.  The 
  8.      * returned value is in megabytes; the baseline Android memory class is 
  9.      * 16 (which happens to be the Java heap limit of those devices); some 
  10.      * device with more memory may return 24 or even higher numbers. 
  11.      */  
  12.     public int getMemoryClass() {  
  13.         return staticGetMemoryClass();  
  14.     }  
  15.   
  16.     /** @hide */  
  17.     static public int staticGetMemoryClass() {  
  18.         // Really brain dead right now -- just take this from the configured  
  19.         // vm heap size, and assume it is in megabytes and thus ends with "m".  
  20.         String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");  
  21.         return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));  
  22.     }  
  23.   
  24.     ......  
  25. }  

        这个函数定义在文件frameworks/base/core/java/android/app/ActivityManager.java中。

        Dalvik虚拟机在启动的时候,就是经过读取系统属性dalvik.vm.heapsize的值来得到Java Object Heap的最大值的,而ActivityManager类的成员函数getMemoryClass最终也经过读取这个系统属性的值来得到Java Object Heap的最大值。

        这个Java Object Heap的最大值也就是咱们平时所说的Android应用程序进程可以使用的最大内存。这里必需要注意的是,Android应用程序进程可以使用的最大内存指的是可以用来分配Java Object的堆。

        Bitmap Memory也称为External Memory,它是用来处理图像的。在HoneyComb以前,Bitmap Memory是在Native Heap中分配的,可是这部份内存一样计入Java Object Heap中,也就是说,Bitmap占用的内存和Java Object占用的内存加起来不能超过Java Object Heap的最大值。这就是为何咱们在调用BitmapFactory相关的接口来处理大图像时,会抛出一个OutOfMemoryError异常的原 因:

[html] view plain copy 在CODE上查看代码片 派生到个人代码片
  1. java.lang.OutOfMemoryError: bitmap size exceeds VM budget  

        在HoneyComb以及更高的版本中,Bitmap Memory就直接是在Java Object Heap中分配了,这样就能够直接接受GC的管理。

        Native Heap就是在Native Code中使用malloc等分配出来的内存,这部份内存是不受Java Object Heap的大小限制的,也就是它能够自由使用,固然它是会受到系统的限制。可是有一点须要注意的是,不要由于Native Heap能够自由使用就滥用,由于滥用Native Heap会致使系统可用内存急剧减小,从而引起系统采起激进的措施来Kill掉某些进程,用来补充可用内存,这样会影响系统体验。

        此外,在HoneyComb以及更高的版本中,咱们能够在AndroidManifest.xml的application标签中增长一个值等于 “true”的android:largeHeap属性来通知Dalvik虚拟机应用程序须要使用较大的Java Object Heap。事实上,在内存受限的手机上,即便咱们将一个应用程序的android:largeHeap属性设置为“true”,也是不能增长它可用的 Java Object Heap的大小的,而即使是能够经过这个属性来增大Java Object Heap的大小,通常状况也不该该使用该属性。为了提升系统的总体体验,咱们须要作的是致力于下降应用程序的内存需求,而不是增长增长应用程序的Java Object Heap的大小,毕竟系统总共可用的内存是固定的,一个应用程序用得多了,就意味意其它应用程序用得少了。

        二. 垃圾收集(GC)

        Dalvik虚拟机能够自动回收那些再也不使用了的Java Object,也就是那些再也不被引用了的Java Object。垃圾自动收集机制将开发者从内存问题中解放出来,极大地提升了开发效率,以及提升了程序的可维护性。

        咱们知道,在C或者C++中,开发者须要手动地管理在堆中分配的内存,可是这每每致使不少问题。例如,内存分配以后忘记释放,形成内存泄漏。又如,非法访 问那些已经释放了的内存,引起程序崩溃。若是没有一个好的C或者C++应用程序开发框架,通常的开发者根本没法驾驭内存问题,由于程序大了以后,很容易造 成失控。最要命的是,内存被破坏的时候,并不必定就是程序崩溃的时候,它就是一颗不定时炸弹,说不许何时会被引爆,所以,查找缘由是很是困难的。

        从这里咱们也能够推断出,Android为何会选择Java而不是C/C++来做来应用程序开发语言,就是为了可以让开发远离内存问题,而将精力集中在 业务上,开发出更多更好的APP来,从而迎头赶超iOS。固然,Android系统内存也存在大量的C/C++代码,这只要考虑性能问题,毕竟C/C++ 程序的运行性能总体上仍是优于运行在虚拟机之上的Java程序的。不过,为了不出现内存问题,在Android系统内部的C++代码码,大量地使用了智能指针来自动管理对象的生命周期。选择Java来做为Android应用程序的开发语言,能够说是技术与商业之间一个折衷,事实证实,这种折衷是成功的。

        回到正题,在GingerBread以前,Dalvik虚拟使用的垃圾收集机制有如下特色:

        1. Stop-the-word,也就是垃圾收集线程在执行的时候,其它的线程都中止;

        2. Full heap collection,也就是一次收集彻底部的垃圾;

        3. 一次垃圾收集形成的程序停止时间一般都大于100ms。

        在GingerBread以及更高的版本中,Dalvik虚拟使用的垃圾收集机制获得了改进,以下所示:

        1.  Cocurrent,也就是大多数状况下,垃圾收集线程与其它线程是并发执行的;

        2.  Partial collection,也就是一次可能只收集一部分垃圾;

        3.  一次垃圾收集形成的程序停止时间一般都小于5ms。

        Dalvik虚拟机执行完成一次垃圾收集以后,咱们一般能够看到相似如下的日志输出:

[html] view plain copy 在CODE上查看代码片 派生到个人代码片
  1. D/dalvikvm(9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms  

        在这一行日志中,GC_CONCURRENT表示GC缘由,2049K表示总共回收的内存,3571K/9991K表示Java Object Heap统计,即在9991K的Java Object Heap中,有3571K是正在使用的,4703K/5261K表示External Memory统计,即在5261K的External Memory中,有4703K是正在使用的,2ms+2ms表示垃圾收集形成的程序停止时间。

        三. 即时编译(JIT)

        前面提到,JIT是相对AOT而言的,即JIT是在程序运行的过程当中进行编译的,而AOT是在程序运行前进行编译的。在程序运行的过程当中进行编译既有好 处,也有坏处。好处在于能够利用程序的运行时信息来对编译出来的代码进行优化,而坏处在于占用程序的运行时间,也就是说不能花太多时间在代码编译和优化之 上。

        为了解决时间问题,JIT可能只会选择那些热点代码进行编译或者优化。根据2-8原则,一个程序80%的时间可能都是在重复执行20%的代码。所以,JIT就能够选择这20%常常执行的代码来进行编译和优化。

        为了充分地利用好运行时信息来优化代码,JIT采用一种激进的方法。JIT在编译代码的时候,会对程序的运行状况进行假设,而且按照这种假设来对代码进行 优化。随着程序的代码,若是前面的假设一直保持成立,那么JIT就什么也不用作,所以就能够提升程序的运行性能。一旦前面的假设再也不成立了,那么JIT就 须要对前面编译优化的代码进行调整,以便适应新的状况。这种调整成本多是很昂贵的,可是只要假设不成立的状况不多或者几乎不会发生,那么得到的好处仍是 大于坏处的。因为JIT在编译和优化代码的时候,对程序的运行状况进行了假设,所以,它所采起的激进优化措施又称为赌博,即Gambling。

        咱们以一个例子来讲明这种Gambling。咱们知道,Java的同步原语涉及到Lock和Unlock操做。Lock和Unlock操做是很是耗时的, 并且它们只有在多线程环境中才真的须要。可是一些同步函数或者同步代码,有程序运行的时候,有可能始终都是被单线程执行,也就是说,这些同步函数或者同步 代码不会被多线程同时执行。这时候JIT就能够采起一种Lazy Unlocking机制。

        当一个线程T1进入到一个同步代码C时,它仍是按照正常的流程来获取一个轻量级锁L1,而且线程T1的ID会记录在轻量锁L1上。当经程T1离开同步函数 或者同步代码时,它并不会释放前面得到的轻量级锁L1。当线程T1再次进入同步代码C时,它就会发现轻量级锁L的全部者正是本身,所以,它就能够直接执行 同步代码C。这时候若是另一个线程T2也要进入同步代码C,它就会发现轻量级锁L已经被线程T1获取。在这种状况下,JIT就须要检查线程T1的调用堆 栈,看看它是否还在执行同步代码C。若是是的话,那么就须要将轻量级锁L1转换成一个重量级锁L2,而且将重量级锁L2的状态设置为锁定,而后再让线程 T2在重量级锁L2上睡眠。等线程T1执行完成同步代码C以后,它就会按照正常的流程来释放重量级锁L2,从而唤醒线程T2来执行同步代码C。另外一方面, 若是线程T2在进入同步代码C的时候,JIT经过检查线程T1的调用堆栈,发现它已经离开同步代码C了,那么它就直接将轻量级锁L1的全部者记录为线程 T2,而且让线程T2执行同步代码C。

        经过上述的Lazy Unlocking机制,咱们就能够充分地利用程序的运行时信息来提升程序的执行性能,这种优化对于静态编译的语言来讲,是没法作到的。从这个角度来看, 咱们就能够说,静态编译语言(如C++)并不必定比在虚拟机上执行的语言(如Java)快,这是由于后者能够有一种强大的武器叫作JIT。

        Dalvik虚拟机从Android 2.2版本开始,才支持JIT,并且是可选的。在编译Dalvik虚拟机的时候,能够经过WITH_JIT宏来将JIT也编译进去,而在启动Dalvik虚拟机的时候,能够经过-Xint:jit选项来开启JIT功能。

        关于虚拟机JIT的实现原理的简要介绍,能够进一步参考这篇文章:http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html

        四. Java本地调用(JNI)

        不管如何,虚拟机最终都是运行在目标机器之上的,也就是说,它须要将本身的指令翻译成目标机器指令来执行,而且有些功能,须要经过调用目标机器运行的操做 系统接口来完成。这样就须要有一个机制,使得函数调用能够从Java层穿越到Native层,也就是C/C++层。这种机制就称为Java本地调用,即 JNI。固然,咱们在执行Native代码的时候,有时候也是须要调用到Java函数的,这一样是能够经过JNI机制来实现。也就是说,JNI机制既支持 在Java函数中调用C/C++函数,也支持在C/C++函数中调用Java函数。

        事实上,Dalvik虚拟机提供的Java运行时库,大部分都是经过调用目标机器操做系统接口来实现的,也就是经过调用Linux系统接口来实现的。例 如,当咱们调用android.os.Process类的成员函数start来建立一个进程的时候,最终会调用到Linux系统提供的fork系统调用来 建立一个进程。

        同时,为了方便开发者使用C/C++语言来开发应用程序,Android官方提供了NDK。经过NDK,咱们就可使用JNI机制来在Java函数中调用 到C/C++函数。不过Android官方是不提倡使用NDK来开发应用程序的,这从它对NDK的支持远远不如SDK的支持就能够看得出来。

        五. 进程和线程管理

        通常来讲,虚拟机的进程和线程都是与目标机器本地操做系统的进程和线程一一对应的,这样作的好处是可使本地操做系统来调度进程和线程。进程和线程调度是 操做系统的核心模块,它的实现是很是复杂的,特别是考虑到多核的状况,所以,就彻底没有必要在虚拟机中提供一个进程和线程库。

        Dalvik虚拟机运行在Linux操做系统之上。咱们知道,Linux操做系统并无纯粹的线程概念,只要两个进程共享同一个地址空间,那么就能够认为 它们同一个进程的两个线程。Linux操做系统提供了两个fork和clone两个调用,其中,前者就是用来建立进程的,然后者就是用来建立线程的。关于 Linux操做系统的进程和线程的实现,能够参考在前面Android学习启动篇一文中提到的经典Linux内核书籍。

        关于Android应用程序进程,它有两个很大的特色,下面咱们就简要介绍一下。

        第一个特色是每个Android应用程序进程都有一个Dalvik虚拟机实例。这样作的好处是Android应用程序进程之间不会相互影响,也就是说,一个Android应用程序进程的意外停止,不会影响到其它的Android应用程序进程的正常运行。

        第二个特色是每个Android应用程序进程都是由一种称为Zygote的进程fork出来的。Zygote进程是由init进程启动起来的,也就是在 系统启动的时候启动的。Zygote进程在启动的时候,会建立一个虚拟机实例,而且在这个虚拟机实例将全部的Java核心库都加载起来。每当Zygote 进程须要建立一个Android应用程序进程的时候,它就经过复制自身来实现,也就是经过fork系统调用来实现。这些被fork出来的Android应 用程序进程,一方面是复制了Zygote进程中的虚拟机实例,另外一方面是与Zygote进程共享了同一套Java核心库。这样不只Android应用程序 进程的建立过程很快,并且因为全部的Android应用程序进程都共享同一套Java核心库而节省了内存空间。

        关于Dalvik虚拟机的特性,咱们就简要介绍到这里。事实上,Dalvik虚拟机和Java虚拟机的实现是相似的,例如,Dalvik虚拟机也支持 JDWP(Java Debug Wire Protocol)协议,这样咱们就可使用DDMS来调试运行在Dalvik虚拟机中的进程。对Dalvik虚拟机的其它特性或者实现原理有兴趣的,建 议均可以参考Java虚拟机的实现,这里提供三本参考书:

        1. Java Virtual Machine Specification (Java SE 7) 

        2. Inside the Java Virtual Machine, Second Edition

        3. Oracle JRockit: The Definitive Guide

        另外,关于Dalvik虚拟机的指令集和dex文件格式的介绍,能够参考官方文档:http://source.android.com/tech/dalvik/index.html。若是对虚拟机的实现原理有兴趣的,还能够参考这个连接:http://www.weibo.com/1595248757/zvdusrg15

        在这里,咱们学习Dalvik虚拟机的目标是打通Java层到C/C++层之间的函数调用,从而能够更好地理解Android应用程序是如何在Linux内核上面运行的。为了达到这个目的,在接下来的文章中,咱们将关注如下四个情景:

        1. Dalvik虚拟机的启动过程

        2. Dalvik虚拟机的运行过程

        3. JNI函数的注册过程

        4. Java进程和线程的建立过程

        掌握了这四个情景以后,再结合前面的全部文章,咱们就能够从上到下地打通整个Android系统了,敬请关注!

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

相关文章
相关标签/搜索