Android性能调优(2)需要掌握Dalvik和ART的知识

学习了JVM之后,就学习一下运行在移动设备上的虚拟机Dalvik和ART。学完DVM和ART就可以冲绘制优化了。
Dalvik和ART也是非常大的体系。对于Android应用开发来说,只需要掌握它们的基本原理并且会看它们的log就行了。
本篇也只介绍基础,如果想深入了解,就得去看专业的书籍。

在Android4.4时ART诞生,DVM和ART在4.4的版本中可以互替,在Android5.0后Android默认运行虚拟机为ART,至此,DVM退出历史舞台。
步入2020年,全球Android用户中,5.0以上的版本占据87~90%,就算DVM已经没有更新了,我们也有必要去学一下DVM,它有很多东西是和ART通用的。而且了解DVM的特点、劣势更方便与我们去探究ART为什么会成为新的低层运行库。

1. Dalvik虚拟机

Dalvik Virtual Machine,即DVM,是Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。

1.1 DVM和JVM的区别

DVM是不是JVM?答案是-----不是!为什么?----因为DVM并没有遵循JVM规范来实现。
通过列出它们的区别,我们能知道DVM的特点。

1.基于架构的不同
JVM是基于栈的,这意味着JVM需要去栈中读写数据,所需的指令会更多。PC平台性能好,所以没什么感觉。但是对于性能受到限制的移动设备,就不是很合适。
DVM是基于寄存器,它不用像基于栈机制一样需要复制数据时而使用的大量的出入栈指令,它的指令更加紧凑、简洁。
但是由于显示指定了操作数,所以基于寄存器的指令会比基于基站的指令要大,但是由于指令数量的数量减少,所以总的代码数不会增加多少。

2.执行的字节码不同
我们知道JVM是执行 .class文件的,然后被JVM识别并直接使用。
DVM中会用 dx工具会将所有的 .class文件转换成一个 .dex文件,然后DVM会从该 .dex文件读取指令和数据。执行顺序为 .java->.class->.dex。
那为什么要这样“多此一举”呢?
这是因为 Java中 .jar文件里面包含了多个 .class文件,当JVM加载这个 .jar时,会去加载里面所有的 .class文件。对于项目特别多特别大的情况下,这样的JVM加载会非常的慢,对于内存优先的移动设备并不合适。
所以 .apk文件里面只有一个.dex文件,这个 .dex文件里会包含所有的 .class文件: dex工具会去除掉 .class所有的冗余信息,并把所有的 .class整合到一个 .dex文件中,减少了I/O操作,加快了类的查找速度。

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

4.DVM由Zygote创建和初始化
Zygote是一个DVM进程,同时也用来创建和初始化 DVM实例。
每当系统需要创建一个应用程序时,Zygote就会fork自身快速创建和初始化一个DVM实例,用于应用程序的运行。
对于一些只读的系统库,所有的DVM实例都会和Zygote共享一块内存区域,节省了内存开销。

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

6.DVM早期没有使用JIT编译器
JVM使用了JIT(Just in time 即时编译器)编译器,而DVM早期没有使用JIT编译器。
早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,然后交给系统,效率并不高。
为了解决这个问题,在Android2.2开始DVM使用JIT,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码,这样在下次执行相同逻辑的时候,直接使用编译之后的本地机器码,而不是每次都需要编译。

需要注意的是,应用程序每次重新运行的时候,都要重做这个编译工作,因此每次重新打开应用程序,都需要JIT编译。

1.2 DVM架构

DVM的源码位于dalvik/目录下,部分目录说明如下所示:

  • Android.mk
    是虚拟机编译的makefile文件
  • vm
    包含虚拟机绝大多数代码,包括虚拟机初始化及内存管理的代码
  • dx
    生成将Java字节码转换为DVM机器码的工具
  • hit
    生成显示堆栈信息/对象信息的工具
  • libdex
    生成主机和设备处理dex文件的库
  • dexopt
    生成dex优化工具
  • dexdump
    生成.dex文件反编译查看工具,主要是用来查看编译出来的代码的正确性和结构
  • dexlist
    此目录是生成查看dex文件里所有类的方法的工具
  • dexgen
    .dex文件代码生成器项目
  • docs
    DVM相关帮助文档
  • tools
    一些编译和运行相关的工具
  • MODULE_LICENSE_APACHE2
    APACHE2版权声明文件
  • NOTICE
    虚拟机源码版权注意事项文件

其中 libdex会被编译成 libdex.a 静态库,作为dex工具使用。DVM架构如下图所示:
在这里插入图片描述

1.3 DVM的运行时堆

DVM的运行时堆使用标记-清除算法进行GC。
它由两个Space以及多个辅助数据结构组成。两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap),下面为他们的说明

  • Zygote Space
    用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC。
    在Zygote进程和应用程序进程之间会共享Zygote Space。在Zygote进程fork第一个子进程之前,会把Zygote Space分成两个部分,原来的已经被使用的那部分堆仍叫Zygote Space,而未使用的那部分堆就叫做Allocation Space
  • Allocation Space
    之后的对象都会在Allocation Space上进行分配和释放。Allocation Space不是进程间共享的,在每个进程中都独立拥有一份。
  • Card Table
    用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息
  • Heap Bitmap
    有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这个GC存活的对象
  • Mark Stack
    DVM的运行时堆使用 标记-清除算法进行GC,Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。

1.4 DVM的GC日志

DVM和ART的GC日志与JVM的日志有很大的区别。
在DVM中每次垃圾的收集都会将GC日志打印到logcat中,具体的格式为:

D/dalvikv: <GC_Reason> <Amount_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文件来分析堆内存时出现的GC
  • GC_EXPLITCIT:显示的GC。例如调用System.gc()
  • GC_EXTERNAL_ALLOC:仅适用API<=10,且用于外部分配内存的GC

2.其他的信息

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

3.实例分析
拿logcat中的GC举个例子:

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

意思就是:引起GC的原因是GC_CONCURRENT,本次GC释放的内存为2012KB,堆的空闲内存百分比为63%,已经使用内存为3213K,堆的总内存为9291K,暂停的总时长为4ms

2.ART虚拟机

ART在Android5.0完全取代了DVM

2.1 ART与DVM的区别

  • DVM中应用每次运行时,字节码都需要通过JIT编译为机器码,这会使得应用程序的运行效率降低,而在ART中系统在安装应用程序时会进行一次 AOT(ahead of time compilation,预编译),将字节码预先编译成机器码并存储在本地,这样应用程序每次运行程序时就不需要执行编译了,运行效率大大的提高了。设备的耗电量也会降低。
    采用AOT预加载也有两个主要的缺点:
    一是AOT会使得应用程序的安装时间边长
    二是字节码预先编译成机器码,机器码需要存储空间会更多一些(这里就是空间换时间了)
    为了解决上面的缺点,在Android7.0的版本中ART加入了JIT,作为AOT的一个补充,在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机器码,从而缩短应用程序的安装时间并节省了存储空间。
  • DVM是为32位CPU设计的,而ART支持64位并兼容32位CPU,这也是DVM被淘汰的主要原因之一
  • ART对垃圾回收机制进行了改进,比如更频繁的执行GC,将GC暂停由两次减小为一次
  • ART的运行时堆空间划分和DVM不同

2.2 ART的运行时堆

与DVM的GC不同的是,ART采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认采用的是CMS(Concurrent Mark-Sweep)方案,该方案主要是使用了 sticky——CMS和 partial-CMS,根据不同的CMS方案,ART的运行时堆的控件也会有不同的划分,默认是由4个Space和多个辅助数据结构组成的。4个Space分别是 Zygote Space、Allocation Space、Image Space和Large Object Space。 前两个和DVM中的作用是一样的。

  • Image Space
    用来存放一些预加载类
  • Large Obejct Space
    用来分配一些大对象(默认大小为12KB)

其中Zygote Space和Image Space是进程间共享的空间。
除了这四个Space,ART的Java堆还包括两个 Mod Union Table,一个Card Table,两个 Heap Bitmap、两个Object Map以及三个Object Stack

2.3 ART的GC日志

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

I/art: <GC_REASON> <GC_NAME> <Objects_freed>(<Size_freed>) AllocSpace Objects, 
<Large_objects_freed>(<Large_object_suze_freed>) <Heap_stats> Losobjects,
<Pause_time(s)>

1.引起GC原因
ART引起GC的原因要比DMV多一些

  • Concurrent
    并发GC,不会使App线程暂停,该GC是在后台线程中进行的,并不会组织内存分配
  • Alloc
    当内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在分配内存的线程中
  • Explicit
    App显示的请求垃圾收集,例如调用System.gc()
    但是我们要避免的去显示调用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运行时修改的对象引用的数量成比例。
    目前,ART的CMS收集器仅有一次暂停,他出现在GC的结尾附近。移动的垃圾收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。

4.实例分析

I/art: Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects,
33% free, 25MB/38MB, paused 1.230ms total 67.1ms

意思就是这一次GC的原因是 Explicit,收集器为CMS,释放对象的数量为104710个,释放的字节数为7MB,释放的大对象为21个共416k,堆的空闲内存占比33%,总内存为38MB,已用25MB,暂停时长1.23ms,GC总时长67.1ms

在AndroidStudio中了可以通过打开Logcat,过滤 “gc”来查看日志:

在这里插入图片描述

3.小结

上面两节,讲述了DVM和ART的特点,他们的结构、运行时堆、GC机制、怎么查看GC日志。
也可以看出DVM和ART的区别,还有这两个虚拟机和JVM的区别。
除此之外,我们可能还需要去了解DVM、ART是怎么产生,由于涉及的源码会关联到 Zygote,而我对Zygote的一些知识还不够清晰,所以在这两个月,我们专门花时间去补关于Zygote的知识。

发布了263 篇原创文章 · 获赞 110 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/104051308