Android Dalvik、ART及APK编译过程

强烈建议看罗升阳大神的文章,五年过去,回头看,真的还是经典啊,不要认为几年前的文章没有价值,看一看就知道 妙啊:
https://blog.csdn.net/Luoshengyang/article/details/41338251
https://blog.csdn.net/Luoshengyang/article/details/41822747
https://blog.csdn.net/Luoshengyang/article/details/42555483
大神为啥是大神:
在这里插入图片描述

一、什么是Dalvik虚拟机

Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,使得Dalvik具有高效、简洁、节省资源的特点。从Android系统架构图知,Dalvik虚拟机运行在Android的运行时库层。

Dalvik作为面向Linux、为嵌入式操作系统设计的虚拟机,主要负责完成对象生命周期管理、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。另外,Dalvik早期并没有JIT编译器,直到Android2.2才加入了对JIT的技术支持。

二、Dalvik虚拟机的特点

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

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

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

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

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

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

三、Dalvik虚拟机和Java虚拟机的区别

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

1、Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。
方法数受限:多个class文件变成一个dex文件所带来的问题就是方法数超过65535时报错,由此引出MultiDex技术,具体资料同学可以google下。
class文件去冗余:class文件存在很多的冗余信息,dex工具会去除冗余信息(多个class中的字符串常量合并为一个,比如对于Ljava/lang/Oject字符常量,每个class文件基本都有该字符常量,存在很大的冗余),并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度。

2、许多GC实现都是在对象开头的地方留一小块空间给GC标记用。
Dalvik VM则不同,在进行GC的时候会单独申请一块空间,以位图的形式来保存整个堆上的对象的标记,在GC结束后就释放该空间。 (关于这一点后面的Dalvik垃圾回收机制还会更加深入的介绍)

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

在这里插入图片描述
3、Java虚拟机与Dalvik虚拟机架构不同

Dalvik可执行文件体积小。Android SDK中有一个叫dx的工具负责将Java字节码转换为Dalvik字节码。

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

针对每个Class文件,都由如下格式进行组成:

在这里插入图片描述
dex格式文件使用共享的、特定类型的常量池机制来节省内存。常量池存储类中的所有字面常量,它包括字符串常量、字段常量等值。
在这里插入图片描述简单来讲,dex格式文件就是将多个class文件中公有的部分统一存放,去除冗余信息。

Java虚拟机与Dalvik虚拟机架构不同。这也是Dalvik与JVM之间最大的区别。

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

(1)dvm速度快!寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。JAVA虚拟机基于栈结构,程序在运行时虚拟机需要频繁的从栈上读取写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU时间。
(2)指令数小!dvm基于寄存器,所以它的指令是二地址和三地址混合,指令中指明了操作数的地址;jvm基于栈,它的指令是零地址,指令的操作数对象默认是操作数栈中的几个位置。这样带来的结果就是dvm的指令数相对于jvm的指令数会小很多,jvm需要多条指令而dvm可能只需要一条指令。
(3)jvm基于栈带来的好处是可以做的足够简单,真正的跨平台,保证在低硬件条件下能够正常运行。而dvm操作平台一般指明是ARM系统,所以采取的策略有所不同。需要注意的是dvm基于寄存器,但是这也是个映射关系,如果硬件没有足够的寄存器,dvm将多出来的寄存器映射到内存中。
4、Dalvik的堆结构相对于JVM的堆结构有所区别。
这主要体现在Dalvik将堆分成了Active堆和Zygote堆,这里大家只要知道Zygote堆是Zygote进程在启动的时候预加载的类、资源和对象(具体gygote进程预加载了哪些类,详见文末的附录),除此之外的所有对象都是存储在Active堆中的。对于为何要将堆分成gygote和Active堆,这主要是因为Android通过fork方法创建到一个新的gygote进程,为了尽可能的避免父进程和子进程之间的数据拷贝,fork方法使用写时拷贝技术,写时拷贝技术简单讲就是fork的时候不立即拷贝父进程的数据到子进程中,而是在子进程或者父进程对内存进行写操作时是才对内存内容进行复制,Dalvik的gygote堆存放的预加载的类都是Android核心类和java运行时库,这部分内容很少被修改,大多数情况父进程和子进程共享这块内存区域。通常垃圾回收重点对Active堆进行回收操作,Dalvik为了对堆进行更好的管理创建了一个Card Table、两个Heap Bitmap和一个Mark Stack数据结构。

四、Dalvik虚拟机的结构

在这里插入图片描述
一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。

1、Dalvik创建对象流程

当Dalvik虚拟机的解释器遇到一个new指令时,它就会调用函数Object* dvmAllocObject(ClassObject* clazz, int flags)。期间完成的动作有( 注意:Java堆分配内存前后,要对Java堆进行加锁和解锁,避免多个线程同时对Java堆进行操作。下面所说的堆指的是Active堆):

调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
执行一次GC, GC执行完毕后,再次调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
首先将堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,然后进行内存分配,成功返回失败下一步。这里调用的函数是 dvmHeapSourceAllocAndGrow
调用函数gcForMalloc来执行GC,这里的GC和第二步的GC,区别在于这里回收软引用对象引用的对象,如果还是失败抛出OOM异常。这里调用的函数是dvmHeapSourceAllocAndGrow

2、Dalvik回收对象流程

Dalvik的垃圾回收策略默认是标记擦除回收算法,即Mark和Sweep两个阶段。标记与清理的回收算法一个明显的区别就是会产生大量的垃圾碎片,因此程序中应该避免有大量不连续小碎片的时候分配大对象,同时为了解决碎片问题,Dalvik虚拟机通过使用dlmalloc技术解决,关于后者读者另行google。下面我们对Mark阶段进行简单介绍。
Mark阶段使用了两个Bitmap来描述堆的对象,一个称为Live Bitmap,另一个称为Mark Bitmap。Live Bitmap用来标记上一次GC时被引用的对象,也就是没有被回收的对象,而Mark Bitmap用来标记当前GC有被引用的对象。当Live Bitmap被标记为1,但是在Mark Bitmap中标记为0的对象表明该对象需要被回收。此外在Mark阶段往往要求其它线程处于停止状态,因此Mark又分为并行和串行两种方式,并行的Mark分为两个阶段:

1)、只标记gc_root对象,即在GC开始的瞬间被全局变量、栈变量、寄存器等所引用的对象,该阶段不允许垃圾回收线程之外的线程处于运行状态。

2)、有条件的并行运行其它线程,使用Card Table记录在垃圾收集过程中对象的引用情况。整个Mark 阶段都是通过Mark Stack来实现递归检查被引用的对象,即在当前GC中存活的对象。标记过程类似用一个栈把第一阶段得到的gc_root放入栈底,然后依次遍历它们所引用的对象(通过出栈入栈),即用栈数据结构实现了对每个gc_root的递归。
Dalvik的GC类型共有四种:

GC_CONCURRENT: 表示是在已分配内存达到一定量之后触发的GC。
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。

其中GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。垃圾回收具体都是通过调用函数void dvmCollectGarbageInternal(const GcSpec* spec) 来执行垃圾回收,该函数的参数GcSpec结构体定义见本文的附录。对于函数dvmCollectGarbageInternal的内部逻辑,即垃圾回收流程,根据垃圾回收线程和工作线程的关系分为并行GC和非并行GC。前者在回收阶段有选择性的停止当前工作线程,后者在垃圾回收阶段停止所有工作线程。但是并行GC需要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操作,意味着并行GC需要花费更多的CPU资源。dvmCollectGarbageInternal函数的内部逻辑如下:(本文末尾的附录中给出了一个对应的流程图)
(1)调用函数dvmSuspendAllThreads挂起所有的线程,以免它们干扰GC。
    这里如何挂起其它线程呢?其实就是每个线程在运行过程中会周期性的检测自身的一个标志位,通过这个标志位我们可以告知线程停止运行。
(2)调用函数dvmHeapBeginMarkStep初始化Mark Stack,并且设定好GC范围。
    Mark Stack其实是一个object指针数组
(3)调用函数dvmHeapMarkRootSet标记根集对象。
Mark的第一阶段,主要分为两大类:

Dalvik虚拟机内部使用的全局对象(维护在一个hash表中);
应用程序正在使用的对象(维护在一个调用栈中)

(4)调用函数dvmClearCardTable清理Card Table。(只在并行gc发生)
    Card Table记录记录在Zygote堆上分配的对象在垃圾收集执行过程中对在Active堆上分配的对象的引用。
(5)调用函数dvmUnlock解锁堆。这个是针对调用函数dvmCollectGarbageInternal执行GC前的堆锁定操作。(只在并行gc发生)
(6)调用函数dvmResumeAllThreads唤醒第1步挂起的线程。(只在并行gc发生)
    此时非gc线程可以开始工作,这部分线程对堆的操作记录在CardTable上面,gc则进行Mark的第二阶段
(7) 调用函数dvmHeapScanMarkedObjects从第3步获得的根集对象开始,归递标记所有被根集对象引用的对象。
(8)调用函数dvmLockHeap重新锁定堆。这个是针对前面第5步的操作。(只在并行gc发生)
(9)调用函数dvmSuspendAllThreads重新挂起所有的线程。这个是针对前面第6步的操作。(只在并行gc发生)
这里需要再次停止工作线程,用来解决前面线程对堆的少部分的操作,这个过程很快。
(10)调用函数dvmHeapReMarkRootSet更新根集对象。因为有可能在第4步到第6步的执行过程中,有线程创建了新的根集对象。(只在并行gc发生)
(11)调用函数dvmHeapReScanMarkedObjects归递标记那些在第4步到第6步的执行过程中被修改的对象。这些对象记录在Card Table中。(只在并行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)调用函数dvmUnlock解锁堆。这个是针对前面第8步的操作。(只在并行gc发生)
(16)调用函数dvmResumeAllThreads唤醒第9步挂起的线程。(只在并行gc发生)
(17)调用函数dvmHeapSweepUnmarkedObjects回收那些没有被引用的对象。没有被引用的对象就是那些在执行第14步之前,在Live Bitmap中的bit设置为1,但是在Mark Bitmap中的bit设置为0的对象。
(18)调用函数dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。这个是针对前面第2步的操作。
(19)调用函数dvmLockHeap重新锁定堆。这个是针对前面第15步的操作。(只在并行gc发生)
(20)调用函数dvmHeapSourceGrowForUtilization根据设置的堆目标利用率调整堆的大小。
(21)调用函数dvmBroadcastCond唤醒那些等待GC执行完成再在堆上分配对象的线程。(只在并行gc发生)
(22)调用函数dvmResumeAllThreads唤醒第1步挂起的线程。(只在非并行gc发生)
(23)调用函数dvmEnqueueClearedReferences将那些目标对象已经被回收了的引用对象增加到相应的Java队列中去,以便应用程序可以知道哪些引用引用的对象已经被回收了。
总结:
通过上面的流程分析,我们知道了并行和串行gc的区别在于:

并行gc会在mark第二阶段将非gc线程唤醒;当mark的第二阶段完成之后,再次停止非gc线程;利用cardtable的信息再次进行一个mark操作,此时的mark操作比第一个mark操作要快得多。
并行gc会在sweep阶段将非gc线程唤醒。
串行gc会在垃圾回收开始就暂停所有非gc线程,知道垃圾回收结束。
并行gc涉及到两次的mark操作,消耗cpu时间。

五、ART虚拟机

在Android5.0中,ART取代了Dalvik虚拟机(安卓在4.4中发布了ART)。ART虚拟机直接执行本地机器码;而Dalvik虚拟机运行的是DEX字节码需要通过解释器执行。安卓运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者重新将自己的应用直接编译成目标机器码,应用程序仍然是一个包含dex字节码的apk文件,这主要得益于AOT技术,AOT(Ahead Of Time)是相对JIT(Just In Time)而言的;也就是在APK运行之前,就对其包含的Dex字节码进行翻译,得到对应的本地机器指令,于是就可以在运行时直接执行了。ART应用安装的时候把dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码。去除了运行时的解释执行,效率更高,启动更快。
  ART运行时内部使用的Java堆的主要组成包括Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map(Live 和 Mark Object Map),以及三个Object Stack (Live、Mark、Allocation Stack)。具体结构图参考附录。
  Image Space和Zygote Space之间,隔着一段用来映射system@framework@boot.art@classes.oat文件的内存。system@framework@boot.art@classes.oat是一个OAT文件,它是由在系统启动类路径中的所有DEX文件翻译得到的,Image Space映射的是一个system@framework@boot.art@classes.dex文件,这个文件保存的是在生成system@framework@boot.art@classes.oat这个OAT文件的时候需要预加载的类对象,这些需要预加载的类由/system/framework/framework.jar文件里面的preloaded-classes文件指定。以后只要系统启动类路径中的DEX文件不发生变化(即不发生更新升级),那么以后每次系统启动只需要将文件system@framework@boot.art@classes.dex直接映射到内存即可。
  由于system@framework@boot.art@classes.dex文件保存的是一些预先创建的对象,并且这些对象之间可能会互相引用,因此我们必须保证system@framework@boot.art@classes.dex文件每次加载到内存的地址都是固定的。这个固定的地址保存在system@framework@boot.art@classes.dex文件开头的一个Image Header中。此外,system@framework@boot.art@classes.dex文件也依赖于system@framework@boot.art@classes.oat文件,因此也会将后者固定加载到Image Space的末尾。
  Image Space是不能分配新对象的。Image Space和Zygote Space在Zygote进程和应用程序进程之间进行共享,而Allocation Space是每个进程都独立地拥有一份。
  ART的运行原理:

1、在Android系统启动过程中创建的Zygote进程利用ART运行时导出的Java虚拟机接口创建ART虚拟机。
2、APK在安装的时候,打包在里面的classes.dex文件会被工具dex2oat翻译成本地机器指令,最终得到一个ELF格式的oat文件。
3、APK运行时,上述生成的oat文件会被加载到内存中,并且ART虚拟机可以通过里面的oatdata和oatexec段找到任意一个类的方法对应的本地机器指令来执行。

oat文件中的oatdata包含用来生成本地机器指令的dex文件内容
oat文件中的oatexec包含有生成的本地机器指令。

注意:
  这里将DEX文件中的类和方法称之为DEX类和DEX方法,将OTA中的类和方法称之为OTA类和OTA方法,ART运行时将类和方法称之为Class和ArtMethod。
  ART中一个已经加载的Class对象包含了一系列的ArtField对象和ArtMethod对象,其中,ArtField对象用来描述成员变量信息,而ArtMethod用来描述成员函数信息。对于每一个ArtMethod对象,它都有一个解释器入口点和一个本地机器指令入口点。

ART找到一个类和方法的流程:

在DEX文件中找到目标DEX类的编号,并且以这个编号为索引,在OAT文件中找到对应的OAT类。
  在DEX文件中找到目标DEX方法的编号,并且以这个编号为索引,在上一步找到的OAT类中找到对应的OAT方法。
  使用上一步找到的OAT方法的成员变量begin_和code_offset_,计算出该方法对应的本地机器指令。
  上面的流程对应给出了流程图,具体内容参考附录。

ART运行时对象的创建过程:

可以分配内存的Space有三个:Zygote Space、Allocation Space和Large Object Space。不过,Zygote Space在还没有划分出Allocation Space之前,就在Zygote Space上分配,而当Zygote Space划分出Allocation Space之后,就只能在Allocation Space上分配。因此实际上应用运行的时候能够分配内存也就Allocation 和 Large Object Space两个。
而分配的对象究竟是存入上面的哪个Space呢?满足如下三个条件的内存,存入Large Object Space:

1)Zygote Space已经划分除了Allocation Space

2)分配对象是原子类型数组,如int[] byte[] boolean[],

3)分配的内存大小大于一定的门限值。
  对于分配对象时内存不足的问题,是通过垃圾回收和在允许范围内增长堆大小解决的。由于垃圾回收会影响程序,因此ART运行时采用力度从小到大的进垃圾回收策略。一旦力度小的垃圾回收执行过后能满足分配要求,那就不需要进行力度大的垃圾回收了。这跟dalvik虚拟机的对象分配策略也是类似的。

ART垃圾回收流程:

1、并行GC流程图如下:

调用子类实现的成员函数InitializePhase执行GC初始化阶段。
获取用于访问Java堆的锁。
调用子类实现的成员函数MarkingPhase执行GC并行标记阶段。
释放用于访问Java堆的锁。
挂起所有的ART运行时线程。
调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象。
恢复第4步挂起的ART运行时线程。
重复第5到第7步,直到所有在GC并行阶段被修改的对象都处理完成。
获取用于访问Java堆的锁。
调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
释放用于访问Java堆的锁。
调用子类实现的成员函数FinishPhase执行GC结束阶段

2、非并行GC流程图如下:

调用子类实现的成员函数InitializePhase执行GC初始化阶段。挂起所有的ART运行时线程。

调用子类实现的成员函数MarkingPhase执行GC标记阶段。
调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
恢复第2步挂起的ART运行时线程。
调用子类实现的成员函数FinishPhase执行GC结束阶段

通过两者的对比可以得出如下结论(与Dalvik大同小异):

非并行GC在垃圾回收的整个过程中暂停了所有非gc线程
并行GC在一开始只是对堆进行加锁,对于那些暂时并不会在堆中分配的内存的线程不起作用,它们依然可以运行,但是会造成对象的引用发生变化,但是这段时间的引用发生的变化被记录了下来。之后系统会停止所有线程,对上面记录的数据进行处理,然后唤起所有线程,系统进入垃圾回收阶段。

六、Android APK 编译打包流程

在这里插入图片描述
1.Java编译器对工程本身的java代码进行编译,这些java代码有三个来源:app的源代码,由资源文件生成的R文件(aapt工具),以及有aidl文件生成的java接口文件(aidl工具)。产出为.class文件。

①.用AAPT编译R.java文件
②编译AIDL的java文件
③把java文件编译成class文件

2…class文件和依赖的三方库文件通过dex工具生成Delvik虚拟机可执行的.dex文件,包含了所有的class信息,包括项目自身的class和依赖的class。产出为.dex文件。

3.apkbuilder工具将.dex文件和编译后的资源文件生成未经签名对齐的apk文件。这里编译后的资源文件包括两部分,一是由aapt编译产生的编译后的资源文件,二是依赖的三方库里的资源文件。产出为未经签名的.apk文件。

4.分别由Jarsigner和zipalign对apk文件进行签名和对齐,生成最终的apk文件。

总结为:编译–>DEX–>打包–>签名和对齐

六、ART虚拟机与Dalvik虚拟机的区别

什么是ART:
ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

ART优点:

1、系统性能的显著提升。
2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。
3、更长的电池续航能力。
4、支持更低的硬件。

ART缺点:

1、更大的存储空间占用,可能会增加10%-20%。
2、更长的应用安装时间。

ART虚拟机相对于Dalvik虚拟机的提升

参考:art和dalvik的区别?

**预编译 **

在dalvik中,如同其他大多数JVM一样,都采用的是JIT来做及时翻译(动态翻译),将dex或odex中并排的dalvik code(或者叫smali指令集)运行态翻译成native code去执行.JIT的引入使得dalvik提升了3~6倍的性能。

而在ART中,完全抛弃了dalvik的JIT,使用了AOT直接在安装时将其完全翻译成native code.这一技术的引入,使得虚拟机执行指令的速度又一重大提升

①垃圾回收机制

首先介绍下dalvik的GC的过程.主要有有四个过程:
1、当gc被触发时候,其会去查找所有活动的对象,这个时候整个程序与虚拟机内部的所有线程就会挂起,这样目的是在较少的堆栈里找到所引用的对象.需要注意的是这个回收动作和应用程序非并发。

2、gc对符合条件的对象进行标记

3、gc对标记的对象进行回收

4、恢复所有线程的执行现场继续运行

dalvik这么做的好处是,当pause了之后,GC势必是相当快速的.但是如果出现GC频繁并且内存吃紧势必会导致UI卡顿,掉帧.操作不流畅等。

后来ART改善了这种GC方式 , 主要的改善点在将其非并发过程改变成了部分并发.还有就是对内存的重新分配管理。

当ART GC发生时:

1、GC将会锁住Java堆,扫描并进行标记

2、标记完毕释放掉Java堆的锁,并且挂起所有线程

3、GC对标记的对象进行回收

4、恢复所有线程的执行现场继续运行

5、重复2-4直到结束

可以看出整个过程做到了部分并发使得时间缩短.据官方测试数据说gc效率提高2倍

提高内存使用,减少碎片化

Dalvik内存管理特点是:内存碎片化严重,当然这也是Mark and Sweep算法带来的弊端
在这里插入图片描述
可以看出每次gc后内存千疮百孔,本来连续分配的内存块变得碎片化严重,之后再分配进入的对象再进行内存寻址变得困难。

ART的解决:在ART中,它将Java分了一块空间命名为Large-Object-Space,这块内存空间的引入用来专门存放large object。同时ART又引入了moving collector的技术,即将不连续的物理内存块进行对齐.对齐了后内存碎片化就得到了很好的解决.Large-Object-Space的引入一是因为moving collector对大块内存的位移时间成本太高,而且提高内存的利用率
根官方统计,ART的内存利用率提高10倍了左右。

Dalvik 和 ART的差异

在Dalvik虚拟环境下,程序的每次运行都会将dex字节码转换为机器码(JIT技术)。这样虽然会降低应用的安装时间(字节码比机器码要小),但是会导致应用的启动时间变慢。

在ART环境下,应用在第一次安装的时候,字节码就会预编译(AOT)成机器码,这样的话,虽然设备和应用的首次启动(安装慢了)会变慢,但是以后每次启动执行的时候,都可以直接运行,因此运行效率会提高。

Dalvik 每次运行都是 .dex字节码→机器码→运行。

而ART是 安装时 .dex字节码→机器码,运行时机器码→运行。

ART相对于Dalvik在GC方面进行了优化

根据官方文档Android Runtime (ART) 和 Dalvik的介绍。 我们可以大概的知道ART不仅在编译机制与Dalvik不同,还在GC方面进行了优化。
在这里插入图片描述ps:在 Dalvik 中,应用时常发现显式调用 System.gc() 有助于促进垃圾回收 (GC)。而在 ART 中,这种做法显得没那么必要,尤其是当您需通过垃圾回收来预防出现 GC_FOR_ALLOC 类型或减少碎片时。您可以通过调用 System.getProperty(“java.vm.version”) 来验证正在使用哪种运行时。如果使用的是 ART,则该属性值应为 “2.0.0” 或更高。
ps2:Android开源项目(AOSP)中正在开发一种紧凑型垃圾回收器,旨在改善内存管理。(什么是紧凑型垃圾回收器?我想到的是操作系统中我们实现动态分区分配的时候就可以采用的是紧凑技术,其实就是挪位置,应该就和这个原理差不多吧!)

参考:https://www.jianshu.com/p/92227738f270

展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值