Android 进阶 ------ JVM DVM ART对比

Android系统使用Dalvik Virtual Machine (DVM)作为其虚拟机,所有安卓程序都运行在安卓系统进程里,每个进程对应着一个Dalvik虚拟机实例。他们都提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能,各自拥有一套完整的指令系统。

Android之所以不直接使用JVM作为其虚拟机的原因有很多,版权问题我们暂且搁置一边,本文将首先在技术上对DVM和JVM进行比较,然后重点对Dalvik虚拟机的垃圾回收机制进行介绍,文章末尾再对Android5.0之后使用的新型虚拟机——ART虚拟机进行简单介绍

DVM vs JVM

共同点:
  • 都是解释执行
  • 都是每个 OS 进程运行一个 VM,并运行一个单独的程序
  • 在较新版本中(Froyo / Sun JDK 1.5)都实现了相当程度的 JIT compiler(即时编译) 用于提速。
    • JIT(Just In Time,即时编译技术)对于热代码(使用频率高的字节码)直接转换成汇编代码;
不同点:
  • dvm执行的是.dex格式文件,jvm执行的是.class文件。class文件和dex之间可以相互转换具体流程如下图,多个class文件转变成一个dex文件会引发一些问题,具体如下:

    • 方法数受限:多个class文件变成一个dex文件所带来的问题就是方法数超过65535时报错,由此引出MultiDex技术,具体资料同学可以google下。
    • class文件去冗余:class文件存在很多的冗余信息,dex工具会去除冗余信息(多个class中的字符串常量合并为一个,比如对于Ljava/lang/Oject字符常量,每个class文件基本都有该字符常量,存在很大的冗余),并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度。
  • 许多GC实现都是在对象开头的地方留一小块空间给GC标记用。Dalvik VM则不同,在进行GC的时候会单独申请一块空间,以位图的形式来保存整个堆上的对象的标记,在GC结束后就释放该空间。 (关于这一点后面的Dalvik垃圾回收机制还会更加深入的介绍)

  • dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。这类的不同是最要命的,因为它将导致一系列的问题,具体如下:
    • dvm速度快!寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。JAVA虚拟机基于栈结构,程序在运行时虚拟机需要频繁的从栈上读取写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU时间。
      • 指令数小!dvm基于寄存器,所以它的指令是二地址和三地址混合,指令中指明了操作数的地址;jvm基于栈,它的指令是零地址,指令的操作数对象默认是操作数栈中的几个位置。这样带来的结果就是dvm的指令数相对于jvm的指令数会小很多,jvm需要多条指令而dvm可能只需要一条指令。
      • jvm基于栈带来的好处是可以做的足够简单,真正的跨平台,保证在低硬件条件下能够正常运行。而dvm操作平台一般指明是ARM系统,所以采取的策略有所不同。需要注意的是dvm基于寄存器,但是这也是个映射关系,如果硬件没有足够的寄存器,dvm将多出来的寄存器映射到内存中。

Dalvik虚拟机

谈到垃圾回收自然而然的想到了堆,Dalvik的堆结构相对于JVM的堆结构有所区别,这主要体现在Dalvik将堆分成了Active堆和Zygote堆,这里大家只要知道Zygote堆是Zygote进程在启动的时候预加载的类、资源和对象(具体gygote进程预加载了哪些类,详见文末的附录),除此之外的所有对象都是存储在Active堆中的。对于为何要将堆分成gygote和Active堆,这主要是因为Android通过fork方法创建到一个新的gygote进程,为了尽可能的避免父进程和子进程之间的数据拷贝,fork方法使用写时拷贝技术,写时拷贝技术简单讲就是fork的时候不立即拷贝父进程的数据到子进程中,而是在子进程或者父进程对内存进行写操作时是才对内存内容进行复制,Dalvik的gygote堆存放的预加载的类都是Android核心类和java运行时库,这部分内容很少被修改,大多数情况父进程和子进程共享这块内存区域。通常垃圾回收重点对Active堆进行回收操作,Dalvik为了对堆进行更好的管理创建了一个Card Table、两个Heap Bitmap和一个Mark Stack数据结构。

  • 关于 Zygote,有一些参考资料
    ZYGOTE浅谈

    Dalvik创建对象流程

    当Dalvik虚拟机的解释器遇到一个new指令时,它就会调用函数Object dvmAllocObject(ClassObject clazz, int flags)。期间完成的动作有( 注意:Java堆分配内存前后,要对Java堆进行加锁和解锁,避免多个线程同时对Java堆进行操作。下面所说的堆指的是Active堆):

    1.调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
    2.执行一次GC, GC执行完毕后,再次调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
    3.首先将堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,然后进行内存分配,成功返回失败下一步。这里调用的函数是dvmHeapSourceAllocAndGrow
    4.调用函数gcForMalloc来执行GC,这里的GC和第二步的GC,区别在于这里回收软引用对象引用的对象,如果还是失败抛出OOM异常。这里调用的函数是dvmHeapSourceAllocAndGrow

Dalvik回收对象流程

Dalvik的垃圾回收策略默认是标记擦除回收算法,即Mark和Sweep两个阶段。标记与清理的回收算法一个明显的区别就是会产生大量的垃圾碎片,因此程序中应该避免有大量不连续小碎片的时候分配大对象,同时为了解决碎片问题,Dalvik虚拟机通过使用dlmalloc技术解决,关于后者读者另行google。下面我们对Mark阶段进行简单介绍。

Mark阶段使用了两个Bitmap来描述堆的对象,一个称为Live Bitmap,另一个称为Mark Bitmap。Live Bitmap用来标记上一次GC时被引用的对象,也就是没有被回收的对象,而Mark Bitmap用来标记当前GC有被引用的对象。当Live Bitmap被标记为1,但是在Mark Bitmap中标记为0的对象表明该对象需要被回收。

此外在Mark阶段往往要求其它线程处于停止状态,因此Mark又分为并行和串行两种方式,并行的Mark分为两个阶段:1)、只标记gc_root对象,即在GC开始的瞬间被全局变量、栈变量、寄存器等所引用的对象,该阶段不允许垃圾回收线程之外的线程处于运行状态。2)、有条件的并行运行其它线程,使用Card
Table记录在垃圾收集过程中对象的引用情况。整个Mark 阶段都是通过Mark Stack来实现递归检查被引用的对象,即在当前GC中存活的对象。标记过程类似用一个栈把第一阶段得到的gc_root放入栈底,然后依次遍历它们所引用的对象(通过出栈入栈),即用栈数据结构实现了对每个gc_root的递归。

文章目录
  1. 1. DVM vs JVM
    1. 1.0.1. 共同点:
    2. 1.0.2. 不同点:
  • 2. Dalvik虚拟机
    1. 2.0.1. Dalvik创建对象流程
  • 2.1. Dalvik回收对象流程
  • |