当前位置: 首页 > 工具软件 > Dalvik > 使用案例 >

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

穆轶
2023-12-01

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方式来编译。

原文:https://my.oschina.net/fhd/blog/365256

 类似资料: