进入Android Dalvik虚拟机之Dalvik虚拟机的特点

雨燕双飞 提交于 2019-11-27 03:20:03

Google于2007年底正式发布了Android SDK,Dalvik虚拟机也第一次进入了人们的视野。它的作者是丹.伯恩斯坦(Dan Bornstein)。Dalvik虚拟机作为Android平台的核心组件,拥有如下几个特点:

  • 体积小,占用内存空间小

  • 专有的DEX可执行文件格式,体积更小,执行速度更快

  • 常量池采用32位索引值,寻址类方法名,字段名,常量更快

  • 基于寄存器架构,并拥有一套完整的指令系统

  • 提供了对象生命周期管理,堆栈管理,线程管理,安全和异常管理以及垃圾回收等重要功能

  • 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例

1. Dalvik虚拟机与Java虚拟机的区别

Dalvik虚拟机与传统的Java虚拟机有着许多不同点,两者并不兼容,它们显著的不同点主要表现在以下几个方面:

  • Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。传统的Java程序经过编译,生成Java字节码保存在class文件中,Java虚拟机通过解码class文件中的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中。Dalvik虚拟机通过解释DEX文件来执行这些字节码。

  • Dalvik可执行文件体积小。Android SDK中有一个叫dx的工具负责将Java字节码转换为Dalvik字节码。dx工具对Java类文件重新排列,消除在类文件中出现的所有冗余信息避免虚拟机在初始化时出现反复的文件加载与解析过程。一般情况下,Java类文件中包含多个不同的方法签名,如果其他的类文件引用该类文件中的方法,方法签名也会被复制到其类文件中,也就是说,多个不同的类会同时包含相同的方法签名,同样地,大量的字符串常量在多个类文件中也被重复使用。这些冗余信息会直接增加文件的体积,同时也会严重影响虚拟机解析文件的效率。消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池。由于dx工具对常量池的压缩,使得相同的字符串,常量在DEX文件中只出现一次,从而减小了文件的体积。

  • Java虚拟机与Dalvik虚拟机架构不同。Java虚拟机基于栈架构,程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。Dalvik虚拟机基于寄存器架构。数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。测试代码如下:

package t1;

public class Hello {
    public static void main(String[] args) {
        Hello hello = new Hello();
        System.out.println(hello.foo(5, 3));
    }
    public int foo(int a,int b) {
        return (a + b) * (a - b);
    }
}

将以上内容保存为Hello.java。打开命令提示符,执行命令:

$ javac -source 1.6 -target 1.6 Hello.java

:如果使用1.7及以上版本的JDK编译Hello.java,生成Hello.class默认的版本会比较低使用dx生成dex文件会提示class文件无效。解决方法是强制指定class文件的版本。

继续上面的讨论,执行上面的命令生成Hello.class文件。然后执行命令:

./dx --dex --output=t1/Hello.dex t1/Hello.class

执行上面的命令前,命令行进入到dx工具所在的目录(位于Android SDK的platform-tools目录中),再把t1/Hello.class目录与文件copy到dx工具所在的目录,然后再执行上面的命令生成dex文件。

接下来在Hello.class所在目录使用javap反编译Hello.class查看foo()函数的Java字节码,执行以下命令:

$ javap -c -cp . Hello.class

命令执行后得到如下代码(foo()函数部分):

public int foo(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: iload_1
       4: iload_2
       5: isub
       6: imul
       7: ireturn

下面再使用dexdump(位于Android sdk的platform-tools目录中)查看foo()函数的Dalvik字节码,执行以下命令:

$ ./dexdump -d t1/Hello.dex

命令执行后整理输出结果,可以得到如下代码(foo()函数部分):

000198:                                             |[000198] t1.Hello.foo:(II)I
0001a8: 9000 0304                                   |0000: add-int v0, v3, v4
0001ac: 9101 0304                                   |0002: sub-int v1, v3, v4
0001b0: b210                                        |0004: mul-int/2addr v0, v1
0001b2: 0f00                                        |0005: return v0

查看上面的Java字节码,发现foo()函数一共占用了8个字节,代码中每条指令占用1个字节。比起Java虚拟机字节码,上面的Dalvik字节码显得简洁很多,只有4条指令就完成了下面的操作。

通过上面的分析 可以发现,基于寄存器架构的Dalvik虚拟机与基于栈架构的Java虚拟机相比,由于生成的代码指令减少了,程序执行速度会更快一些

2. Dalvik虚拟机是如何执行程序的

Android系统由Linux内核,函数库,Android运行时,应用程序框架以及应用程序组成。Dalvik虚拟机属行Android运行时环境,它与一些核心库共同承担Android应用程序的运行工作。

Android系统启动加载完内核后,第一个执行的是init进程,init进程首先要做的是设备的初始化工作,然后读取inic.rc文件并启动系统中的重要外部程序 Zygote。Zygote进程是Android所有进程的孵化器进程,它启动后会首先初始化Dalvik虚拟机,然后启动system_server并进入Zygote模式,通过socket等候命令。当执行一个Android应用程序时,system_server进程通过Binder IPC方式发送命令给Zygote,Zygote收到命令后通过fork自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,这样一个程序就启动完成了。

Zygote提供了三种创建进程的方法:

  • fork(),创建一个Zygote进程(这种方式实际不会被调用)

  • forkAndSpecialize(),创建一个非Zygote进程

  • forkSystemServer(),创建一个系统服务进程。

其中,Zygote进程可以再fork()出其他进程,非Zygote进程则不能fork其他进程,而系统服务进程在终止后它的子进程也必终止。当进程fork成功后,执行的工作就交给了Dalvik虚拟机。Dalvik虚拟机首先通过loadClassFromDex()函数完成类的装载工作,每个类被成功解析后都会拥有一个ClassObject类型的数据结构存储在运行时环境中,虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类,随后,字节码验证器使用dvmVerifyCodeFlow()函数对装入的代码进行校验,接着虚拟机调用FindCass()函数查找并装载main方法类,随后调用dvmInterpret()函数初始化解释器并执行字节码流。

3. 关于Dalvik虚拟机JIT(即时编译)

JIT(Just-in-time Compilation,即时编译),又称为动态编译,是一种通过在运行时将字节码翻译为机器码的技术,使得程序的执行速度更快。Android2.2版本系统的Dalvik虚拟机引入了JIT技术,官方宣称新版的Dalvik虚拟机比以往执行速度快3~6倍。主流的JIT包含两种字节码编译方式

  • method方式:以函数或方法为单位进行编译。

  • trace方式:以trace为单位进行编译

method方式很好理解,那什么是trace方式呢?在函数中一般很少是顺序执行代码的,多数的代码都分成了好几条执行路径,其中函数的有些路径在实际运行过程中是很少被执行的,这部分路径被称为“冷路径”,而执行比较频繁的路径被称为“热路径”。采用传统的method方式会编译整个方法的代码,这会使得在“冷路径”上浪费很多编译时间,并且耗费更多的内存;trace方法编译则能够快速地获取“热路径”代码,使用更短的时间与更少的内存来编译代码

目前,Dalvik虚拟机默认采用trace方式编译代码,同时也支持采用method方式来编译。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!