Dalvik虚拟机学习之路

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

Dalvik虚拟机使用的是dex(Dalvik Executable)格式的类文件,而Java虚拟机使用的是class格式的类文件。一个dex文件可以包含若干个类,而一个class文件只包括一个类。由于一个dex文件可以包含若干个类,因此它就可以将各个类中重复的字符串和其它常数只保存一次,从而节省了空间,这样就适合在内存和处理器速度有限的手机系统中使用。

Dalvik虚拟机使用的指令是基于寄存器的,而Java虚拟机使用的指令集是基于堆栈的。

寄存器(Register),是中央处理器内的其中组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器。在中央处理器的算术及逻辑部件中,包含的寄存器有累加器

每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,其代码在虚拟机的解释器下得以执行。

有一个特殊的虚拟机进程Zygote,他是虚拟机实例的孵化器。它在系统启动的时候就会产生,它会完成虚拟机的初始化、库的加载、预制类库和初始化的操作。如果系统需要一个新的虚拟机实例,它会迅速复制自身,以最快的速度提供给系统。对于一些只读的系统库,所有虚拟机实例都和Zygote共享一块内存区域。

2.Dalvik的工作流程

Dalvik虚拟机支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。(dx 是一套工具,可以将 Java .class 转换成 .dex 格式. 一个dex档通常会有多个.class。由于dex有时必须进行最佳化,会使档案大小增加1-4倍,以ODEX结尾。)


Dalvik工作模型

2.1 java-class

首先读取源码,一个一个字节的读取进来,找出来我们Java定义的关键字,比如if ,else,for,while,finally,等这个步骤就是叫做词法分析过程

第二步:检查第一步读取出来的关键字是否符合Java语言规范,比如if后面跟的是不是一个Boolean类型的表达式,这个过程就叫做语法分析

第三步:经过以上2个步骤词法分析,语法分析,基本上已经按照Java规范了,接下来就是这些拼装的代码要表达什么意思,也就是语义分析


class字节码模型

注:Java源码中的类名,方法名,变量名,居然都是以字符串形式存储在常量池中。所以,图class字节码模型中的this_class和super_class分别指向两个字符串,代表本类的名字和基类的名字。

常量池数组的元素类型

常量池结构

常量池常见类型:

CONSTANT_Utf8_info:就是字符串

CONSTANT_Class_info:类信息

CONSTANT_NameAndType_Info:用来描述方法/成员名以及类型信息的

Methodref_Info,InterfaceMethodref_Info,Fieldref_Info

用于描述方法、接口信息和成员变量。

Methodref_Info,InterfaceMethodref_Info,Fieldref_Info数据结构

常量池就不说这么多了,有兴趣的就自行解析。解析方法为:javap -verbose xxxx.class

2.2 class-dex

前言:Android平台中没有直接使用Class文件格式,是因为早期的Anrdroid手机内存,存储都比较小,而Class文件显然有很多可以优化的地方,比如每个Class文件都有一个常量池,里边存储了一些字符串。一串内容完全相同的字符串很有可能在不同的Class文件的常量池中存在,这就是一个可以优化的地方。

传统Class文件是一个Java源码文件会生成一个.Class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,如此,多个Class文件里如果有重复的字符串,当把它们都放到一个dex文件的时候,只要一份就可以了嘛。


dex文件模型

2.3 dex-odex

odex文件就是dex文件具体在某个系统(不同手机,不同手机的OS,不同版本的OS等)上的优化。odex文件的优化依赖系统上的几个核心模块(由BOOTCLASSPATH环境变量给出,一般是/system/framework/下的jar包,尤其是core.jar),主要还是为了提高Dalvik虚拟机的运行速度,这部分内容了解即可。

odex文件模型

3.内存管理

3.1物理内存

物理内存即移动设备上的RAM,当启动一个Android程序时,会启动一个Dalvik VM进程,系统会给它分配固定的内存空间(16M,32M不定),这块内存空间会映射到RAM上某个区域。然后这个Android程序就会运行在这块空间上。Java里会将这块空间分成Stack栈内存和Heap堆内存。stack里存放对象的引用,heap里存放实际对象数据。

注:android使用了pagingmemory-mapping(mmapping)的机制来管理内存。这意味着任何你修改的内存(无论是通过分配新的对象还是去访问mmaped pages中的内容)都会贮存在RAM中,而且不能被paged out。因此唯一完整释放内存的方法是释放那些你可能hold住的对象的引用,当这个对象没有被任何其他对象所引用的时候,它就能够被GC回收了。只有一种例外是:如果系统想要在其他地方重用这个对象。

3.1.1Java Object Heap

Java Object Heap是用来分配Java对象的,也就是我们在代码new出来的对象都是位于Java Object Heap上的。Dalvik虚拟机在启动的时候,可以通过-Xms和-Xmx选项来指定Java Object Heap的最小值和最大值。可以通过ActivityManager类的成员函数getMemoryClass来获得Dalvik虚拟机的Java Object Heap的最大值。Android应用程序进程能够使用的最大内存指的是能够用来分配Java Object的堆。

3.1.2Bitmap Memory 

它是用来处理图像的。在3.0以及更高的版本中,Bitmap Memory就直接是在Java Object Heap中分配了,这样就可以直接接受GC的管理。

3.1.3Native Heap

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

4.垃圾收集

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

Dalvik虚拟机使用Mark-Sweep算法来进行垃圾收集。顾名思义,Mark-Sweep算法就是为Mark和Sweep两个阶段进行垃圾回收。其中,Mark阶段从根集(Root Set)开始,递归地标记出当前所有被引用的对象,而Sweep阶段负责回收那些没有被引用的对象。

当Dalvik虚拟机成功地在堆上分配一个对象之后,会检查一下当前分配的内存是否超出一个阀值。

GC_FOR_MALLOC:表示是在堆上分配对象时内存不足触发的GC。

GC_CONCURRENT:表示是在已分配内存达到一定量之后触发的GC。

GC_EXPLICIT:表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。

GC_BEFORE_OOM:表示是在准备抛OOM异常之前进行的最后努力而触发的GC。

GC工作模型

Dalvik虚拟机支持非并行和并行两种GC。在图中,左边是非并行GC的执行过程,而右边是并行GC的执行过程。它们的总体流程是相似的,主要差别在于前者在执行的过程中一直是挂起非GC线程的,而后者是有条件地挂起非GC线程。

第1步到第3步用于并行和非并行GC:

1.  调用函数dvmSuspendAllThreads挂起所有的线程,以免它们干扰GC。

2.  调用函数dvmHeapBeginMarkStep初始化Mark Stack,并且设定好GC范围。

Mark Stack具体来说,当我们标记完成根集对象之后,就按照它们的地址从小到大的顺序标记它们所引用的其它对象。假设有A、B、C和D四个对象,它的地址大小关系为A < B < C < D,其中,B和D是根集对象,A被D引用,C没有被B和D引用。那么我们将依次遍历B和D。当遍历到B的时候,没有发现它引用其它对象,然后就继续向前遍历D对象。发现它引用了A对象。按照递归的算法,这时候除了标记A对象是正在使用之外,还应该去检查A对象有没有引用其它对象,然后又再检查它引用的对象有没有又引用其它的对象,一直这样遍历下去。这样就跟函数递归一样。更好的做法是将对象A记录在一个Mark Stack中,然后继续检查地址值比对象D大的其它对象。对于地址值比对象D大的其它对象,如果它们引用了一个地址值比它们小的其它对象,那么这些其它对象同样要记录在Mark Stack中。等到该轮检查结束之后,再回过头来检查记录在Mark Stack里面的对象。然后又重复上述过程,直到Mark Stack等于空为止。

3.  调用函数dvmHeapMarkRootSet标记根集对象。

第4到第6步用于并行GC:

4.  调用函数dvmClearCardTable清理Card Table。Card Table由Card组成,一个Card实际上就是一个字节,它的值要么是CLEAN,要么是DIRTY。因为接下来我们将会唤醒第1步挂起的线程。并且使用这个Card Table来记录那些在GC过程中被修改的对象。

5.  调用函数dvmUnlock解锁堆。这个是针对调用函数dvmCollectGarbageInternal执行GC前的堆锁定操作。

6.  调用函数dvmResumeAllThreads唤醒第1步挂起的线程。

第7步用于并行和非并行GC:

7.  调用函数dvmHeapScanMarkedObjects从第3步获得的根集对象开始,归递标记所有被根集对象引用的对象。

第8步到第11步用于并行GC:

8.  调用函数dvmLockHeap重新锁定堆。这个是针对前面第5步的操作。

9.  调用函数dvmSuspendAllThreads重新挂起所有的线程。这个是针对前面第6步的操作。

10. 调用函数dvmHeapReMarkRootSet更新根集对象。因为有可能在第4步到第6步的执行过程中,有线程创建了新的根集对象。

11. 调用函数dvmHeapReScanMarkedObjects归递标记那些在第4步到第6步的执行过程中被修改的对象。这些对象记录在Card Table中。

第12步到第14步用于并行和非并行GC:

12. 调用函数dvmHeapProcessReferences处理那些被软引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的对象,以及重写了finalize方法的对象。这些对象都是需要特殊处理的。

13. 调用函数dvmHeapSweepSystemWeaks回收系统内部使用的那些被弱引用引用的对象。

14. 调用函数dvmHeapSourceSwapBitmaps交换Live Bitmap和Mark Bitmap。执行了前面的13步之后,所有还被引用的对象在Mark Bitmap中的bit都被设置为1。而Live Bitmap记录的是当前GC前还被引用着的对象。通过交换这两个Bitmap,就可以使得当前GC完成之后,使得Live Bitmap记录的是下次GC前还被引用着的对象。

第15步和第16步用于并行GC:

15. 调用函数dvmUnlock解锁堆。这个是针对前面第8步的操作。

16. 调用函数dvmResumeAllThreads唤醒第9步挂起的线程。

第17步和第18步用于并行和非并行GC:

17. 调用函数dvmHeapSweepUnmarkedObjects回收那些没有被引用的对象。没有被引用的对象就是那些在执行第14步之前,在Live Bitmap中的bit设置为1,但是在Mark Bitmap中的bit设置为0的对象。

18. 调用函数dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。这个是针对前面第2步的操作。

第19步用于并行GC:

19. 调用函数dvmLockHeap重新锁定堆。这个是针对前面第15步的操作。

第20步用于并行和非并行GC:

20. 调用函数dvmHeapSourceGrowForUtilization根据设置的堆目标利用率调整堆的大小。

第21步用于并行GC:

21. 调用函数dvmBroadcastCond唤醒那些等待GC执行完成再在堆上分配对象的线程。

第22步用于非并行GC:

22. 调用函数dvmResumeAllThreads唤醒第1步挂起的线程。

第23步用到并行和非并行GC:

23. 调用函数dvmEnqueueClearedReferences将那些目标对象已经被回收了的引用对象增加到相应的Java队列中去,以便应用程序可以知道哪些引用引用的对象已经被回收了。


5.进程与线程管理

Dalvik虚拟机运行在Linux操作系统之上。我们知道,Linux操作系统并没有纯粹的线程概念,只要两个进程共享同一个地址空间,那么就可以认为它们同一个进程的两个线程。Linux操作系统提供了两个fork和clone两个调用,其中,前者就是用来创建进程的,而后者就是用来创建线程的。

Dalvik虚拟机线程的创建过程

5.1Thread.start

Thread类的成员函数start首先检查成员变量hasBeenStarted的值是否等于true。如果等于true的话,那么就说明当前正在处理的Thread对象所描述的Java线程已经启动起来了。一个Java线程是不能重复启动的,否则的话,Thread类的成员函数start就会抛出一个类型为IllegalThreadStateException的异常。通过了上面的检查之后,Thread类的成员函数start接下来就继续调用VMThread类的静态成员函数create来创建一个线程。

5.2VMThread.create

VMThread类的静态成员函数create是一个JNI方法,它将Java层传递过来的参数获取出来之后,就调用另外一个函数dvmCreateInterpThread来执行创建线程的工作。

5.3dvmCreateInterpThread

将用来描述新创建的Dalvik虚拟机线程的Native层的Thread对象保存在gDvm.threadList所描述的一个线程列表中,这是因为当前所有Dalvik虚拟机线程都保存在这个列表中。

将新创建的Dalvik虚拟机线程的状态设置为THREAD_VMWAIT,使得新创建的Dalvik虚拟机线程继续往前执行,这是因为新创建的Dalvik虚拟机线程将自己的状态设置为THREAD_STARTING唤醒创建它的线程之后,又会等待创建它的线程通知它继续往前执行。

5.4interpThreadStart

1. 调用函数prepareThread来初始化新创建的Dalvik虚拟机线程。

2. 将新创建的Dalvik虚拟机线程的状态设置为THREAD_STARTING,以便其父线程,也就是创建它的线程可以继续往前执行。

3. 通过一个while循环来等待父线程通知自己继续往前执行,也就是等待父线程将自己的状态设置为THREAD_VMWAIT。

4. 调用函数dvmCreateJNIEnv来为新创建的Dalvik虚拟机线程创建一个JNI环境。

5. 调用函数dvmChangeStatus将新创建的Dalvik虚拟机线程的状态设置为THREAD_RUNNING,表示它正式进入运行状态。

6. 如果此时gDvm.debuggerConnected的值等于true,那么就说明有调试器连接到当前Dalvik虚拟机来了,这时候就调用函数dvmDbgPostThreadStart来通知调试器新创建了一个线程。

7. 调用函数dvmChangeThreadPriority来设置新创建的Dalvik虚拟机线程的优先级,这个优先级值保存在用来描述新创建的Dalvik虚拟机线程的一个Java层Thread对象的成员变量priority中。

8. 找到Java层的java.lang.Thread类的成员函数run,并且通过函数dvmCallMethod来交给Dalvik虚拟机解释器执行,这个java.lang.Thread类的成员函数run即为Dalvik虚拟机线程的Java代码入口点函数。

9. 从函数dvmCallMethod返回来之后,新创建的Dalvik虚拟机线程就完成自己的使命了,这时候就可以调用函数dvmDetachCurrentThread来执行清理工作。

6.Dalvik部分源码分析


start指令对应的代码

jni_invocation.Init:初始化JNI相关的几个重要函数。通过dlopen加载libdvm.so。看来每个Java进程都会有这个东西。这可是dalvik vm的核心库。这个库有很多API,我个人觉得如果了解libdvm.so的话,应该能干很多事情。这里就不讲那么仔细了。

startVm:注意,它传入了一个JNIEnv* env对象进去,当这个函数返回时,我们在JNI中天天见的JNIEnv对象就是这个东西。startVm是Dalvik VM的核心,该函数返回后,VM就基本就绪了。其实startVm方法做的事情就是初始化VM核心数据结构。讲一下跟目前有相关的知识点,实际上,根据Java VM规范,类的唯一性由全路径类名+定义它的ClassLoader两者唯一确定。

startReg:注册Android平台中一些特有的JNI函数。有兴趣的可以深入研究。

 
dvmStartup指令对应的代码

dvmStartup函数是在startVm函数内的虚拟机创建的核心。

dvmStartup首先是解析参数,这些参数信息可能会传给gDvm相关的成员变量。解析参数是由setCommandLineDefaults和processOptions来完成的。代码就不看了,就讲几个关键参数。

gDvm.executionMode = kExecutionModeJit:如果定义的WITH_JIT宏,则执行模式是JIT模式。

gDvm.bootClassPathStr:由BOOTCLASSPATH环境变量提供。讲一下BOOTCLASSPATH值指向是什么,system/framework下几乎所有的jar包都被放在了BOOT CLASSPATH里。

gDvm.mainThreadStackSize = kDefaultStackSize。kDefaultStackSize值为16K,代表主线程的堆栈大小

gDvm.dexOptMode = OPTIMIZE_MODE_VERIFIED,用于控制odex操作,该参数表示只对verified的类进行odex。

接下来一堆startup函数中的重点,dvmClassStartup函数

dvmClassStartup函数相应的代码

先讲一下dvmClassStartup函数做成了什么事情:

创建了一个Hash表,用来存储已经加载的类。

创建了代表java.lang.Class和所有基础数据类型的Class信息。

processClassPath这个函数,它要加载所有的Boot Class,它涉及到system/framework/下的jar包的加载,加载完毕后,虚拟机启动的流程差不多就完了。

接下来讲的是Class的加载和初始化:

new-instance指令对应的代码

先调用dvmDexGetResolvedClass,看看目标类TestAnother是不是已经被解析过了。前面曾经提到说,一个类在初始化的时候可能会解析它所使用到的其他类。

假设被引用的类没有解析过,则调用dvmResolveClass来加载目标类。

目标类加载成功后,如果该类没有初始化过,则调用dvmInitClass进行初始化。

dvmResolveClass其主要逻辑就是先得到目标类名(Lcom/test/TestAnother;)然后调用dvmFindClassNoInit来加载目标类。

dvmFindClassNoInit其主要逻辑就是由于referrer的ClassLoader(也就是使用TestAnother类的TestMain类的ClassLoader)不为空,代码逻辑将走到findClassFromLoaderNoInit。

findClassFromLoaderNoInit其主要逻辑就是调用java/lang/ClassLoader的loadClass函数来加载类。

加载成功后,接下来就是初始化了、dvmInitClass从函数名就能知道它的作用了。

dvmInitClass函数对应的代码

这只是Dalvik虚拟机学习之路的一个简易整理版本,如果你想深入学习Dalvik虚拟机,这些内容还是不够的。还有很多东西并没有讲到,还需要大家继续努力。

参考地址:

http://www.infoq.com/cn/articles/android-in-depth-dalvik?utm_source=infoq&utm_campaign=user_page&utm_medium=link

http://blog.csdn.net/luoshengyang/article/details/8852432

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容