Dalvik 和 ART

    技术2023-07-15  110

    1.Dalvik 虚拟机

    Dalvik 虚拟机(Dalvik Virtual Machine ),简称 Dalvik VM 或者 DVM它是由 Dan Bomstein编写的,名字源于他的祖先居住过的名为Dalvik的小渔村。DVMGoogle专门 为Android平台开发的虚拟机,它运行在Android运行时库中。需要注意的是DVM并不是 一个Java虚拟机(以下简称JVM),至于为什么,下文会给你答案。

    1.DVMJVM的区别

    DVM之所以不是一个JVM,主要原因是DVM并没有遵循JVM规范来实现,DVM JVM主要有以下区别。

    1.基于的架构不同

    JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度变慢, 对于性能有限的移动设备,显然不是很适合的。DVM是基于寄存器的,它没有基于栈的虚 拟机在复制数据时而使用的大量的出入栈指令,同时指令更紧凑、更简洁。但是由于显式 指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少, 总的代码数不会增加多少。

    2 .执行的字节码不同

    Java SE程序中,Java类被编译成一个或多个.class文件,并打包成jar文件,而后 JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为.java文件->.class 文件->.jar文件,而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM 会从该.dex文件读取指令和数据。执行顺序为.java文件->.class文件->.dex文件。

    如图11-1所示,.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的 常量池、类信息、属性等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件, JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。而在.apk文件中只包含了 一个.dex文件,这个.dex文件将所有的.class里面所包含的信息全部整合在一起了,这样再 加载就加快了速度。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有 的.class文件整合到.dex文件中,减少了 I/O操作,加快了类的査找速度。

     

    3.DVM允许在有限的内存中同时运行多个进程

    DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用 都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间中,独立的 进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

    4.DVMZygote创建和初始化

    我们在第2章学习过Zygote,它是一个DVM进程,同时也用来创建和初始化DVM 实例。每当系统需要创建一个应用程序时,Zygote就会fock自身,快速地创建和初始化一 个DVM实例,用于应用程序的运行。对于一些只读的系统库,所有的DVM实例都会和 Zygote共享一块内存区域,节省了内存开销。

    5.DVM有共享机制

    DVM拥有预加载一共享的机制,不同应用之间在运行时可以共享相同的类,拥有更高 的效率。而JVM机制不存在这种共享机制,不同的程序,打包以后的程序都是彼此独立的, 即便它们在包里使用了同样的类,运行时也都是单独加载和运行的,无法进行共享。

    6.DVM早期没有使用JIT编译器

    JVM使用了 JIT编译器(Just In Time Compiler,即时编译器),而DVM早期没有使用 JIT编译器。早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,然 后交给系统处理,效率不是很高。为了解决这一问题,从Android 2.2版本开始DVM使用 了 J1T编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器 码Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码, 而不是每次都需要编译。需要注意的是,应用程序每一次重新运行的时候,都要重做这个 编译工作,因此每次重新打开应用程序,都需要JIT编译。

    2.DVM 架构

    DVM的源码位于dalvik/目录下,Android 8.0中的DVM源码的部分目录说明如表11-1 所示。

                                                                          表11-1 DVM的源码目录

    目录/文件

    说 明

    dexdump

    生成dex文件的反编译査看工具,主要用来査看编译出来的代码的正确性和结构

    dexgen

    dex代码生成器项目

    docs

    DVM相关帮助文档

    dx

    Java字节码转换为DVM机器码的工具

    libdex

    生成主机和设备处理dex文件的库

    tools

    一些编译和运行相关的工具

    Android.mk

    虚拟机编译的makefile配置文件

    MODULE LICENSE APACHE2

    APACHE2版权声明文件

    NOTICE

    虚拟机源码版权注意事项文件

    其中,dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump .dex文件的反编译工具,DVM架构如图11-2所示。

                                                                                            图11-2 DVM架构

    从图11-2可以看出,首先Java编译器编译的.class文件经过DX工具转换为.dex文 件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行, 最后交于Linux处理。

    3.DVM的运行时堆

    DVM的运行时堆使用标记一清除(Mark-Sweep)算法进行GC,它由两个Space以及 多个辅助数据结构组成,两个Space分别是Zygote Space (Zygote Heap)Allocation Space (Active Heap)o Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象, Zygote Space中不会触发GC,Zygote进程和应用程序进程之间会共享Zygote SpaceoZygote进程fork第一个子进程之前,会把Zygote Space分为两个部分,原来的已经被使用 的那部分堆仍旧叫Zygote Space,而未使用的那部分堆就叫Allocation Space,以后的对象 都会在Allocation Space JL进行分配和释放。Allocation Space不是进程间共享的,在每个进 程中都独立拥有一份。除了这两个Space,还包含以下数据结构。

    Card Table用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。Heap Bitmap有两个Heap Bitmap, 一个用来记录上次GC存活的对象,另一个用 来记录这次GC存活的对象。Mark Stack DVM的运行时堆使用标记一清除(Mark-Sweep)算法进行GC, Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。

    4.DVM GC 日志

    10.6.2节中提到了 Java虚拟机的GC日志。DVMARTGC日志与Java虚拟机 的日志有较大的区别。在DVM中每次垃圾收集都会将GC日志打印到logcat中,具体的 格式为:

    D/dalvikvm: <GC_Reason> <2kmount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

    可以看到DVM的日志共有5个信息,其中GC Reason有很多种,这里将它单独拿出来进行介绍。

    1.引起GC的原因

    GC Reason就是引起GC的原因,有以下几种。

    GC_CONCURRENT当堆开始填充时,并发GC可以释放内存。GC_FOR_MALLOC当堆内存已满时,App尝试分配内存而引起的GC,系统必须 停止App并回收内存。GC_HPROF_DUMP_HEAP:当你请求创建HPROF文件来分析堆内存时出现的GCOGC_EXPLICIT显式的GC,例如调用System.gc()(应该避免调用显式的GC,信 任GC会在需要时运行)。GC_EXTERNAL_ALLOC:仅适用于API级别小于等于10,且用于外部分配内存的 GCO

    2.其他的信息

    除了引起GC的原因,其他的信息如下。

    Amount freed本次GC释放内存的大小。Heap stats堆的空闲内存百分比(已用内存)/ (堆的总内存)。Extemal_memory_stats: API小于等于级别10的内存分配(已分配的内存)/ (引起 GC的阈值)。Pause time暂停时间,更大的堆会有更长的暂停时间。并发暂停时间会显示两个暂 停时间,即一个出现在垃圾收集开始时,另一个出现在垃圾收集快要完成时。

    3.实例分析

    为了让大家更好地理解DVMGC 0志,举一个具体的GC日志实例,如下所示:

    D/dalvikvm: GC_CONCURRENT freed 2012K, 63% free 3213K/9291K, external 4501K/5161K, paused 2ms+2ms

    这个GC日志的含义为:引起GC的原因是GC_CONCURRENT本次GC释放的内存 为2012KB堆的空闲内存百分比为63%,已用内存为3213KB,堆的总内存为9291KB 暂停的总时长为4ms

    ART虚拟机

    ART (Android Runtime)虚拟机是 Android 4.4 发布的,用来替换 Dalvik 虚拟机,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ARTAndroid 5.0版本中默认采 用了 ART, DVM从此退出历史舞台。

    1.ARTDVM的区别

    ARTDVM的区别主要有如下4点

    从11.1节我们知道,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART中,系统在安装应用程序时会 进行一次AOT (ahead of time compilation,预编译),将字节码预先编译成机器码并存储在 本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电 量也会降低。这就好比我们在线阅读漫画,DVM是我们阅读到哪就加载哪,ART则是直 接加载一章的漫画,虽然一开始加载速度有些慢,但是后续的阅读体验会很流畅。采用AOT 也会有缺点,主要有两个:第一个是AOT会使得应用程序的安装时间变长,尤其是一些复 杂的应用;第二个是字节码预先编译成机器码,机器码需要的存储空间会多一些。为了解 决上面的缺点,Android 7.0版本中的ART加入了即时编译器JIT,作为AOT的一个补充, 在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机 器码,从而缩短应用程序的安装时间并节省了存储空间。DVM是为32CPU设 计的,而ART支持64位并兼容32CPU,这也是DVM 被淘汰的主要原因之一。ART对垃圾回收机制进行了改进,比如更频繁地执行并行垃圾收集,将GC暂停 由2次减少为1次等。ART的运行时堆空间划分和DVM不同。

    2.ART的运行时堆

    与DVMGC不同的是,ART采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了 CMS( Concurrent Mark-Sweep)方案,该方案主要使用了 sticky-CMS partial-CMS根据不同的CMS方案,ART的运行时堆的空间也会有不同的划分,默认 是由4Space和多个辅助数据结构组成的,4Space分别是Zygote SpaceAllocation SpaceImage Space Large Object Spaceo Zygote SpaceAllocation Space DVM 中的作 用是一样的,Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象 (默认大小为12KB),其中Zygote SpaceImage Space是进程间共享的。采用标记一清除算法的运行时堆空间划分如图11-3所示。

    除了这四个Space, ARTJava堆中还包括两个Mod Union Table, 一个Card Table, 两个 Heap Bitmap,两个 Object Map,以及三个 Object Stacko

    3.ART 的 GC 日志

    ART的GC H志与DVM不同,ART会为那些主动请求的垃圾收集事件或者认为GC 速度慢时才会打印GC日志。GC速度慢指的是GC暂停超过5ms或者GC持续时间超过 100mso如果App未处于可察觉的暂停进程状态,那么它的GC不会被认为是慢速的。

    ART的GC H志具体的格式为:

    I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>

    下面对GC日志的组成部分进行介绍。

    1. 引起GC原因

    ART的引起GC原因(GC_Reason)要比DVM多一些,有以下几种。

    Concurrent:并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不 会阻止内存分配。Alloc:当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在 分配内存的线程中。Explicit: App显示的请求垃圾收集,例如调用System.gc()oDVM 一样,最佳做 法是应该信任GC并避免显式地请求GC,显式地请求GC会阻止分配线程并不必要 地浪费CPU周期。如果显式地请求GC导致其他线程被抢占,那么有可能会导致jank (App同一帧画了多次)。NativeAlloc: Native内存分配时,比如为Bitmaps或者RenderScript分配对象,这会 导致Native内存压力,从而触发GC。CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器 转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集 器转换仅在以下情况下出现:在内存较小的设备上,App将进程状态从可察觉的暂 停状态变更为可察觉的非暂停状态(反之亦然)。HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间, 通常发生在当App已经移动到可察觉的暂停进程状态时。这样做的主要原因是减少 了内存使用并对堆内存进行碎片整理。DisableMovingGc:不是真正触发GC的原因,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,收集会被阻塞。在一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。HeapTrim:不是触发GC的原因,但是请注意,收集会一直被阻塞,直到堆内存整 理完毕。

    2.垃圾收集器名称

    GC_Name指的是垃圾收集器名称,有以下几种。

    Concurrent Mark Sweep (CMS): CMS收集器是一种以获取最短收集暂停时间为目 标的收集器,采用了标记一清除算法实现。它是完整的堆垃圾收集器,能释放除了 Image Space外的所有的空间。Concurrent Partial Mark Sweep:部分完整的堆垃圾收集器,能释放除了 Image Space Zygote Space外的所有空间。Concurrent Sticky Mark Sweep:粘性收集器,基于分代的垃圾收集思想,它只能释 放自上次GC以来分配的对象。这个垃圾收集器比一个完整的或部分完整的垃圾收 集器扫描得更频繁,因为它更快并且有更短的暂停时间。Marksweep + Semispace非并发的GC,复制GC用于堆转换以及齐性空间压缩(堆 碎片整理)。

    3.其他信息

    Objects freed:本次GC从非Large Object Space中回收的对象的数量。Size freed:本次GC从非Large Object Space中回收的字节数。Large objects freed:本次 GC Large Object Space 中回收的对象的数量。Large object size freed:本次 GC Large Object Space 中回收的字节数。Heap stats:堆的空闲内存百分比,即(已用内存)/ (堆的总内存)。Pause times暂停时间,暂停时间与在GC运行时修改的对象引用的数量成比例。 目前,ARTCMS收集器仅有一次暂停,它岀现在GC的结尾附近。移动的垃圾 收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。 

     4.实例分析 

    I/art : Explicit concurrent mark sweep GC free d 104710(7MB) AllocSpace objects,

    21 (416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

    这个GC 日志的含义为引起GC原因是Explicit垃圾收集器为CMS收集器;释放对 象的数量为104710个,释放字节数为7MB释放大对象的数量为21个,释放大对象字节 数为416KB堆的空闲内存百分比为33%,已用内存为25MB,堆的总内存为38MB GC 暂停时长为1.230ms, GC总时长为67.216ms。  

    3.DVM 和ART的诞生

    虽然DVMART的知识体系非常庞大,但是我们仍旧有必要了解DVM是怎么来的。 在2.1.5节中讲过init启动Zygote时会调用app main.cppmain函数,如下所示:

    frameworks/base/cmds/app_process/app_main.cpp

    int main(int argc, char* const argv[]){ if (zygote) {//1 runtime.start (ncom.android. internal.os.Zygotelnit1', args, zygote); //2 else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote); else { fprintf(stderr, ''Error: no class name or --zygote supplied. \n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); return 10; } }

    在注释1处如果为ture,就说明当前程序运行在Zygote进程中,在注释2处调用 AppRuntime start 函数,start 函数具体在 AppRuntime 的父类 AndroidRuntime 中实现,如 下所示:

    frameworks/base/core/jni/AndroidRuntime.cpp

    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){ Jnilnvocation jni_invocation; jni_invocation.Init(NULL);//1 JNIEnv* env; //启动Java虚拟机 if (startVm(&mJavaVM, &env, zygote) != 0) (//2 return; } onVmCreated(env); //为Java虚拟机注册JNI方法 if (startReg(env) < 0) {//3 ALOGE("Unable to register all android natives\n"); return; } }

    在注释2处调用startVm函数来创建Java虚拟机,在注释3处调用startReg函数来为 Java虚拟机注册JNI方法。在注释1处调用了 jni invocationInit函数:

    libnativehelper/Jnilnvocation.cpp

    bool Jniinvocation::Init(const char* library) ( #ifdef _ANDROID_ char buffer[PROP_VALUE_MAX]; #else char* buffer = NULL; #endif library = GetLibrary(library, buffer);//l const int KDlopenFlags = RTLD_NOW | RTLD_NODELETE; handle_ = dlopen(library, kDlopenFlags);//2 if (handle_ == NULL) { if (strcmp(library, kLibraryFallback) == 0) ( ALOGE("Failed to dlopen %s: %s", library, dlerror()); return false; } } return true; }

    在注释1处调用了 GetLibrary函数:

    libnativehelper/Jnilnvocation.cpp

     

    #ifdef ANDROID//1 static char* kLibrarySystemProperty = "persist.sys.dalvik.vm.lib.2”;//2 static char* kDebuggableSystemProperty = "ro.debuggableM; #endif static const char* kLibraryFallback = "libart.so"; template<typename T> void UNUSED(const T&) (} const char* Jnilnvocation::GetLibrary(const char* library, char* buffer) { #ifdef ANDROID_ const char* default_library; char debuggable[PROP_VALUE_MAX]; system_property_get(kDebuggableSystemProperty, debuggable); if (strcmp(debuggable, "1") != 0) {//3 library = kLibraryFallback;//4 default_library = kLibraryFallback;//5 } else { if (buffer != NULL) { if (system_property_get(kLibrarySystemProperty, buffer) > 0) {//6 default_library = buffer; } else { default_library = kLibraryFallback; } } else { default_library = kLibraryFallback; } } #else UNUSED(buffer); const char* default_library = kLibraryFallback;//7 #endif if (library = NULL) { library = default_library;//8 } return library; }

    注释1处代表在Android平台,注释2处的persist.sys.dalvik.vm.lib.2是一个系统属性, 它的取值可以为libdvm.so或者libart.so,值为libdvm.so说明当前用的是DVM,值为libart.so 说明当前用的是ARTO在注释3处如果debuggable不等于”1”,说明当前不是Debug模式 构建的,是不允许动态更改虚拟机动态库的。在注释4和注释5处将libart.so赋值给library 和default library0如果是Debug模式构建会在注释6处读取persist.sys.dalvik.vm.lib.2配置中是否有传入的参数buffer,如果有就将default library赋值为buffer,如果没有将 default library赋值为libart.so。在注释7处如果不是在Android平台,default library的值 为libart.so0在注释8处如果library为NULL就将default !ibrary赋值给library并返回该 library。这里我们知道Android 8.0如果不是Debug模式构建,只能返回libart.so。回到 Jnilnvocation的Init函数,注释1处的GetLibrary函数会返回libart.so或者libdvm.so,接着 在注释2处调用dlopen函数来加载libart.so或者libdvm.so。因此我们知道Jnilnvocation的Init函数的主要作用是初始化ART或者DVM的环境,初始化完毕后会调用startVm函数来 启动相应的虚拟机。讲到这里我们应该知道DVM和ART是如何诞生的了,没错,是在 Zygote进程中诞生的,这样Zygote进程就持有了 DVM或者ART的实例,此后Zygote进 程fork自身创建应用程序进程时,应用程序进程也得到了 DVM或者ART的实例,这样就 不需要每次启动应用程序进程都要创建DVM或者ART,从而加快了应用程序进程的启动 速度。

    本章小结

    本章介绍了 DVM和ART的基本原理、如何阅读它们的log和DVM,以及ART的诞生。阅读本章前请阅读第2章、第3章和第10章会有利于对本章的理解。DVM和ART的 知识体系完全可以写一本书,如果想要更多地了解它们请阅读专业的书籍和博客,博客推 荐老罗(罗升阳)的博客,里面有一系列文章专门介绍DVM和ART,虽然文章基于的Android 版本有些老,但仍具有参考价值。

    Processed: 0.010, SQL: 9