Dalvik虚拟机和ART虚拟机
最后更新于:2022-04-02 05:04:14
### **概述**
Android应用程序是运行在Dalvik虚拟机里面的,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。Android应用程序中的Dalvik虚拟机实例实际上是从Zygote进程的地址空间拷贝而来的,这样就可以加快Android应用程序的启动速度。Dalvik虚拟机与Java虚拟机共享有差不多的特性,例如,它们都是解释执行,并且支持即时编译(JIT)、垃圾收集(GC)、Java本地方法调用(JNI)和Java远程调试协议(JDWP)等,差别在于两者执行的指令集是不一样的,并且前者的指令集是基本寄存器的,而后者的指令集是基于堆栈的。
本系列文章主要将Dalvik虚拟机的内存管理、垃圾收集、即时编译、Java本地调用、进程和线程管理等。理解Dalvik虚拟机的上述实现细节,有助于在运行时修改程序的行为,例如,拦截Java函数的调用。
* Dalvik虚拟机概述
* Dalvik虚拟机的启动过程
* Dalvik虚拟机的运行过程
* JNI函数的注册过程
* Dalvik虚拟机进程
* Dalvik虚拟机线程
#### **Dalvik虚拟机概述**
Dalvik虚拟机由Dan Bornstein开发,名字来源于他的祖先曾经居住过的位于冰岛的同名小渔村
Dalvik虚拟机起源于Apache Harmony项目,后者是由Apache软件基金会主导的,目标是实现一个独立的、兼容JDK 5的虚拟机,并根据Apache License v2发布
**Dalvik虚拟机与Java虚拟机的区别**

* 基于堆栈的Java指令(1个字节)和基于寄存器的Dalvik指令(2、4或者6个字节)各有优劣
* 一般而言,执行同样的功能, Java虚拟机需要更多的指令(主要是load和store指令),而Dalvik虚拟机需要更多的指令空间
* 需要更多指令意味着要多占用CPU时间,而需要更多指令空间意味着指令缓冲(i-cache)更易失效
* Dalvik虚拟机使用dex(Dalvik Executable)格式的类文件,而Java虚拟机使用class格式的类文件
* 一个dex文件可以包含若干个类,而一个class文件只包括一个类
* 由于一个dex文件可以包含若干个类,因此它可以将各个类中重复的字符串只保存一次,从而节省了空间,适合在内存有限的移动设备使用
* 一般来说,包含有相同类的未压缩dex文件稍小于一个已经压缩的jar文件
**Dex文件的生成**

**Dex文件的优化**
* 将invoke-virtual指令中的method index转换为vtable index – 加快虚函数调用速度
* 将get/put指令中的field index转换为byte offset – 加快实例成员变量访问速度
* 将boolean/byte/char/short变种的get/put指令统一转换为32位的get/put指令 – 减小VM解释器的大小,从而更有效地利用CPU的i-cache
* 将高频调用的函数,例如String.length,转换为inline函数 – 消除函数调用开销
* 移除空函数,例如Object.`` -- 消除空函数调用
* 将可以预先计算的数据进行预处理,例如预先生成VM根据class name查询class的hash table – 节省Dex文件加载时间以及内存占用空间
* 将invoke-virtual指令中的method index转换为vtable index
~~~
invoke-virtual {v1, v2}, method@BBBB
→
invoke-virtual-quick {v1,v2},vtable #0xhh
~~~
* 将get/put指令中的field index转换为byte offset
~~~
iget-object v0, v2, field@BBBB
→
iget-object-quick v0,v2,[obj+0x100]
~~~
备注:
http://www.haogongju.net/art/1171347
http://mylifewithandroid.blogspot.com/2009/05/about-quick-method-invocation.html
http://source.android.com/devices/tech/dalvik/dex-format.html
**Dex文件的优化时机**
* VM在运行时即时优化,例如使用DexClassLoader动态加载dex文件时。这时候需要指定一个当前进程有写权限的用来保存odex的目录。
* APP安装时由具有root权限的installd优化。这时候优化产生的odex文件保存在特权目录/data/dalvik-cache中。
* 编译时优化。这时候编译出来的jar/apk里面的classes.dex被提取并且优化为classes.odex保存在原jar/apk所在目录,打包在system image中。
**内存管理**
* Java Object Heap
大小受限,16M/24M/32M/48M/…
* Bitmap Memory(External Memroy):
大小计入Java Object Heap
* Native Heap
大小不受限
**Java Object Heap**
* 用来分配Java对象。Dalvik虚拟机在启动的时候,可以通过-Xms和-Xmx选项来指定Java Object Heap的最小值和最大值。
* Java Object Heap的最小和最大默认值为2M和16M。但是厂商会根据手机的配置情况进行调整,例如,G1、Droid、Nexus One和Xoom的Java Object Heap的最大值分别为16M、24M、32M 和48M。
* 通过ActivityManager.getMemoryClass可以获得Dalvik虚拟机的Java Object Heap的最大值。
**Bitmap Memory**
* 用来处理图像。在HoneyComb之前,Bitmap Memory是在Native Heap中分配的,但是这部分内存同样计入Java Object Heap中。这就是为什么我们在调用BitmapFactory相关的接口来处理大图像时,会抛出一个OutOfMemoryError异常的原因:**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掉某些进程,用来补充可用内存,这样会影响系统体验。
**垃圾收集(GC)**
* Step 1: Mark,使用RootSet标记对象引用
* Step 2: Sweep,回收没有被引用的对象
**GingerBread之前**
* Stop-the-word,也就是垃圾收集线程在执行的时候,其它的线程都停止
* Full heap collection,也就是一次收集完全部的垃圾
* 一次垃圾收集造成的程序中止时间通常都大于100ms
**GingerBread之后**
* Cocurrent,也就是大多数情况下,垃圾收集线程与其它线程是并发执行的
* Partial collection,也就是一次可能只收集一部分垃圾
* 一次垃圾收集造成的程序中止时间通常都小于5ms
* Dalvik虚拟机执行完成一次垃圾收集之后,我们通常可以看到类似以下的日志输出:
~~~
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)**
* 从2.2开始支持JIT,并且是可选的,编译时通过WITH_JIT宏进行控制
* 基于执行路径(Executing Path)对热门的代码片断进行优化(Trace JIT),传统的Java虚拟机以Method为单位进行优化(Method JIT)
* 可以利用运行时信息进行激进优化,获得比静态编译语言更高的性能,如Lazy Unlocking机制,可以参考《Oracle JRockit: The Definitive Guide》一书
* 实现原理:http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html
* 支持JDWP(Java Debug Wire Protocol)协议
* 每一个Dalvik虚拟机进程都都提供有一个端口来供调试器连接
* DDMS提供有一个转发端口8870,通过它可以同时调试多个Dalvik虚拟机进程

#### **Dalvik虚拟机的启动过程**
Dalvik虚拟机由Zygote进程启动,然后再复制到System Server进程和应用程序进程

* startVM的过程中会创建一个JavaVMExt,并且该JavaVMExt关联有一个JNIInvokeInterface,Native Code通过它来访问Dalvik虚拟机

* startVM的过程中还会为当前线程关联有一个JNIEnvExt,并且该JNIEnvExt 关联有一个JNINativeInterface,Native Code通过它来调用Java函数或者访问Java对象

**Dalvik虚拟机在Zygote进程启动的过程中,还会进一步预加载Java和Android核心类库以及系统资源**

**Dalvik虚拟机从Zygote进程复制到System Server进程之后,它们就通过COW(Copy On Write)机制共享同一个Dalvik虚拟机实例以及预加载类库和资源**

**Dalvik虚拟机从Zygote进程复制到应用程序进程之后,它们同样会通过COW(Copy On Write)机制共享同一个Dalvik虚拟机实例以及预加载类库和资源**

#### **Dalvik虚拟机的运行过程**
* Dalvik虚拟机在Zygote进程中启动之后,就会以ZygoteInit.main为入口点开始运行
* Dalvik虚拟机从Zygote进程复制到System Server进程之后,就会以SystemServer.main为入口点开始运行
* Dalvik虚拟机Zygote进程复制到应用程序进程之后,就会以ActivityThread.main为入口点开始运行
* 上述入口点都是通过调用JNINativeInterface接口的成员函数CallStaticVoidMethod来进入的
J
**JNINativeInterface->CallStaticVoidMethod对应的实现为CallStaticVoidMethodV**

**CallStaticVoidMethodV调用dvmCallMethodV**

**在Dalvik虚拟机中,无论是Java函数,还是Native函数,都是通过Method结构体来描述的**

> **备注**:
> struct Method定义在文件dalvik/vm/oo/Object.h中
**在Dalivk虚拟机中,通过dvmIsNativeMethod判断一个函数是Java函数还是Native函数**

> **备注**:
> dvmIsNativeMethod定义在文件dalvik/vm/oo/Object.h中
**Native函数直接由CPU执行,Java函数由Dalvik虚拟机解释执行,即通过dvmInterpret函数执行**

**Dalvik虚拟机标准解释器:dvmInterpretStd**

**Invoke-direct指令由函数invokeDirect执行**

**函数invokeDirect调用 invokeMethod执行**

#### **JNI函数的注册过程**
**JNI函数注册示例 -- ClassWithJni**

**JNI函数注册示例 -- shy_luo_jni_ClassWithJni_nanosleep**

**System.loadLibrary**

**Runtime.loadLibrary**

**Runtime.nativeLoad**

**dvmLoadNativeCode**

**JNI_OnLoad**

**jniRegisterNativeMethods**

**RegisterNatives**

**dvmRegisterJNIMethod**

**dvmUseJNIBridge**

**dvmSetNativeFunc**

#### **Dalvik虚拟机进程**
* Dalvik虚拟机进程与下层的Linux进程是一一对应的
* 当ActivityManagerService启动一个组件的时候,发现用来运行该组件的应用程序进程不存在,就会请求Zygote进程创建
* Zygote进程通过调用Zygote类的成员函数forkAndSpecialize来创建
**Zygote.forkAndSpecialize**


**forkAndSpecializeCommon**

* Dalvik虚拟机线程与下层的Linux线程是一一对应的
* 在Java层中,可以创建一个Thread对象,并且调用该Thread对象的成员函数start来启动一个Dalvik虚拟机线程
* 在Native层中,也可以通过创建一个Thread对象,并且调用该Thread对象的成员函数run来启动一个Dalvik虚拟机线程
**在Java层创建Dalvik虚拟机线程--Thread.start**

**VMThread.create**

**dvmCreateInterpThread**

**线程启动函数:interpThreadStart**

**dvmCreateJNIEnv**

**在Native层创建Dalvik虚拟机线程--Thread::run**

**createThreadEtc**

**androidCreateThreadEtc**

> **注意**,函数指针gCreateThreadFn所指向的函数在Dalvik虚拟机启动时已经被修改为javaCreateThreadEtc
**javaCreateThreadEtc**

**androidCreateRawThreadEtc**

**AndroidRuntime::javaThreadShell**

**javaAttachThread**

**AttachCurrentThread**

**attachThread**

**dvmAttachCurrentThread**

';