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结尾。)
2.1 java-class
首先读取源码,一个一个字节的读取进来,找出来我们Java定义的关键字,比如if ,else,for,while,finally,等这个步骤就是叫做词法分析过程
第二步:检查第一步读取出来的关键字是否符合Java语言规范,比如if后面跟的是不是一个Boolean类型的表达式,这个过程就叫做语法分析
第三步:经过以上2个步骤词法分析,语法分析,基本上已经按照Java规范了,接下来就是这些拼装的代码要表达什么意思,也就是语义分析
注: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文件的时候,只要一份就可以了嘛。
2.3 dex-odex
odex文件就是dex文件具体在某个系统(不同手机,不同手机的OS,不同版本的OS等)上的优化。odex文件的优化依赖系统上的几个核心模块(由BOOTCLASSPATH环境变量给出,一般是/system/framework/下的jar包,尤其是core.jar),主要还是为了提高Dalvik虚拟机的运行速度,这部分内容了解即可。
3.内存管理
3.1物理内存
物理内存即移动设备上的RAM,当启动一个Android程序时,会启动一个Dalvik VM进程,系统会给它分配固定的内存空间(16M,32M不定),这块内存空间会映射到RAM上某个区域。然后这个Android程序就会运行在这块空间上。Java里会将这块空间分成Stack栈内存和Heap堆内存。stack里存放对象的引用,heap里存放实际对象数据。
注:android使用了paging与memory-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。
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两个调用,其中,前者就是用来创建进程的,而后者就是用来创建线程的。
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部分源码分析
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函数是在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函数做成了什么事情:
创建了一个Hash表,用来存储已经加载的类。
创建了代表java.lang.Class和所有基础数据类型的Class信息。
processClassPath这个函数,它要加载所有的Boot Class,它涉及到system/framework/下的jar包的加载,加载完毕后,虚拟机启动的流程差不多就完了。
接下来讲的是Class的加载和初始化:
先调用dvmDexGetResolvedClass,看看目标类TestAnother是不是已经被解析过了。前面曾经提到说,一个类在初始化的时候可能会解析它所使用到的其他类。
假设被引用的类没有解析过,则调用dvmResolveClass来加载目标类。
目标类加载成功后,如果该类没有初始化过,则调用dvmInitClass进行初始化。
dvmResolveClass其主要逻辑就是先得到目标类名(Lcom/test/TestAnother;)然后调用dvmFindClassNoInit来加载目标类。
dvmFindClassNoInit其主要逻辑就是由于referrer的ClassLoader(也就是使用TestAnother类的TestMain类的ClassLoader)不为空,代码逻辑将走到findClassFromLoaderNoInit。
findClassFromLoaderNoInit其主要逻辑就是调用java/lang/ClassLoader的loadClass函数来加载类。
加载成功后,接下来就是初始化了、dvmInitClass从函数名就能知道它的作用了。
这只是Dalvik虚拟机学习之路的一个简易整理版本,如果你想深入学习Dalvik虚拟机,这些内容还是不够的。还有很多东西并没有讲到,还需要大家继续努力。
参考地址: