Dalvik 和 ART

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文件来分析堆内存时出现的GCO
  • GC_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点

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值