ART uses ahead-of-time (AOT) compilation, and starting in Android 7.0 (Nougat or N), it uses a hybrid ==combination of AOT, just-in-time (JIT) compilation, and profile-guided compilation==. The combination of all these compilation modes is ==configurable== and will be discussed in this section. As an example, Pixel devices are configured with the following compilation flow:
JIT和AOT共存
ART comprises a compiler (the dex2oat tool) and a runtime (libart.so) that is loaded for starting the Zygote. The dex2oat tool takes an APK file and generates one or more compilation artifact files that the runtime loads. The number of files, their extensions, and names are subject to change across releases, but as of the Android O release, the files being generated are:
Compilation options for ART are of two categories:
One core ART option to configure these two categories is compiler filters. Compiler filters drive how ART compiles DEX code and is an option passed to the dex2oat tool. Starting in Android O, there are four officially supported filters:
https://source.android.com/devices/tech/dalvik/jit-compiler
Android runtime (ART) includes a just-in-time (JIT) compiler with code profiling that continually improves the performance of Android applications as they run. ==The JIT compiler complements ART’s current ahead-of-time (AOT) compiler and improves runtime performance, saves storage space, and speeds application and system updates.== It also improves upon the AOT compiler by avoiding system slowdown during automatic application updates or recompilation of applications during over-the-air (OTA) updates.
Although JIT and AOT use the same compiler with a similar set of optimizations, the generated code might not be identical. ==JIT makes use of runtime type information, can do better inlining, and makes on stack replacement (OSR) compilation possible==, all of which generates slightly different code.
Figure 1. JIT architecture.
JIT compilation involves the following activities:
Figure 2. Profile-guided compilation.
Android 平台的绝大多数应用是使用 Java 语言写的,CPU 只能理解汇编指令,无法直接识别 Java语言的虚拟机指令;为了让 CPU 能运行 Java语言编写的程序,一般有两种办法:
「计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决」引入一个中间层,这个中间层负责 Java代码的执行,然后这个中间层本身编译为 ==CPU 能理解的汇编指令==,也就是 CPU -> 中间层 -> Java 代码。如果这个中间层采用 Java 语言直接作为输入,理解一句 Java 语句就把Java语言翻译一下让CPU 执行一段,我们一般称这种模式为「解释执行」。毋庸置疑这种方式效率是相当低效的。
直接把 Java 语言翻译成==CPU能理解的机器语言==。这里又有两种方式: 在程序运行之前直接把 Java 代码编译为机器语言。这种模式我们称之为 AOT (Ahead of time)编译。 在程序运行起来之后,实时地把 Java 语言编译为机器语言然后执行。这种模式称之为 JIT(Just in time) 编译。
背景介绍完了回到 Android 平台上面,Android 平台分为几个阶段:
应用安装的时候不执行 AOT 编译,安装速度飞快。初次使用应用的时候没有机器码,因此只能解释执行。
应用运行起来之后,系统收集经常被运行的代码的信息,做两件事:1)在必要的时候在运行时直接把 Java 代码编译为机器码 (JIT),然后使用机器码执行提高运行效率。2)把这个「经常被运行的代码信息保存起来」
设备空闲的时候,系统拿出应用运行时候保存的「热点代码信息」直接把这些代码编译为机器码 (AOT)
关于 Android 7.0 系统的演进可以参阅这里:http://s3.amazonaws.com/connect.linaro.org/las16/Presentations/Tuesday/LAS16-201%20-%20ART%20JIT%20in%20Android%20N.pdf
8.0上改进了解释器,解释模式执行效率大幅提升;
Android 9.0上提供了预先放置热点代码的方式,应用在安装的时候就能知道常用代码会被提前编译。可以看到,当前 Android 平台的执行模式在空间占用+安装速度+运行速度上已经达到了一个很好的平衡。
Android 源码分析(十) Dalvik 虚拟机创建过程
虚拟机 理解Android虚拟机体系结构
4.2 Dalvik类加载器 一个dex文件需要类加载器加载原生类和Java类,然后通过解释器根据指令集对Dalvik字节码进行解释和执行。Dalvik类加载器使用mmap函数,将dex文件映射到内存中,通过普通的内存读取操作即可访问dex文件,然后解析dex文件内容并加载其中的类到哈希表中。
4.2.1 解析dex 总的来说,dex文件可以抽象为三个部分:头部、索引、数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行分析,分析的结果放到DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向class索引的起始位置。为了加快class的查找速度,还创建一个哈希表,对class名字进行哈希并生成索引。
4.2.2 加载class 解析工作完成后就进行class的加载,加载的类需要用ClassObject数据结构来存储。
typedef struct Object {
ClassObject* clazz; // 类型对象
Lock lock; // 锁对象
} Object;
其中clazz指向ClassObject对象,还包含一个Lock对象。如果其它线程想要获取它的锁,只有等这个线程释放。Dalvik每加载一个class都会对应一个ClassObject对象,加载过程会在内存中分配几个区域,分别存放directMethod, virtualMethod, sfield, ifield。这些信息从dex文件的数据区中读取。字段Field的定义如下:
struct Field {
ClassObject* clazz; //所属类型
const char* name; // 变量名称
const char* signature; // 如“Landroid/os/Debug;”
u4 accessFlags; // 访问标记
#ifdef PROFILE_FIELD_ACCESS
u4 gets;
u4 puts;
#endif
};
待得到class索引后,实际的加载由loadClassFromDex来完成。首先它会读取class的具体数据,分别加载directMethod, virtualMethod, ifield和sfield,然后为ClassObject数据结构分配内存,并读取dex文件的相关信息。加载完成后,将加载的class通过dvmAddClassToHash函数放入哈希表,以方便下次查找;最后,通过dvmLinkClass查找该类的超类,如果有接口类则加载相应的接口类。
4.3 Dalvik解释器 对于任何虚拟机来说,解释器无疑是核心的部分,所有的Java字节码都经过解释器解释执行。由于Dalvik解释器的效率很重要,Android分别实现了C语言版和各种汇编语言版的解释器。解释器通常是循环执行,需要一个入口函数调用处理程序执行第一条指令,而后每条指令执行时引出下一条指令,通过函数指针调用处理程序。
5 Android的启动
启动电源,加载引导程序到RAM
BootLoader引导
Linux Kernel启动
Init进程创建
Init fork出Zygote进程,Zygote进程创建虚拟机;创建系统服务
Android Home Launcher启动
第一个命门
Java的“虚拟机”
前面提到,Java为了能够实现跨平台操作,便借助虚拟机来调度硬件平台资源。在虚拟机里,还需要集成翻译器或者编译器,来将Java的字节码(即中间代码)解释成机器听得懂的机器语言,或者直接编译成机器直接执行的010101的机器码。
2008年,Android 1.0刚发布的时候,使用的是一个叫Dalvik的虚拟机,里面集成了一个解释器,每次用户在安卓手机上运行APP时,就会叫醒这个解释器,来给安卓的硬件解释APP想要干嘛。这就相当于新闻发布会,发言人讲一句自己的母语,然后再由专业翻译将其翻译成外国记者听得懂的语言,效率非常低下,一个小时可能也问不了几个问题。
谷歌意识到这个问题严重拖了安卓手机的后腿,所以通过一年多的努力,在2010年中发布了2.2版本,引入了JIT(Just in Time,即时编译)机制。JIT比较聪明,当用户在安卓手机运行APP时,会同时将用户经常使用的功能编译为机器能直接执行的010101机器码,不用每一句每一句的去翻译。当出现不常用的功能时,再把解释器叫起来翻译。
JIT虽然变聪明了一点,但是每次启动APP都要先编译一次,不能一劳永逸。加上Dalvik虚拟机性能比较落后,所以谷歌在2014年10月推出了Android 5.0版本,将虚拟机从Dalvik替代成ART(Android Run Time),同时把JIT的编译器替代成AOT (Ahead of Time)。意思就是说,APP在下载后安装到手机上时同时把能编译的代码先编译成机器听得懂的101010。剩下不太好翻译的代码,就在用户使用时再叫醒解释器来翻译。AOT相比JIT的好处,就是不用每次打开APP都需要先编译一遍。但是,坏处就是用户安装APP的时间有点长。
越来越多的用户吐槽为什么安装一个APP也慢吞吞。于是,谷歌在2017年Android 7.0又做了一点改进,安装时先不编译中间代码,而是在用户空闲时将能够编译成机器码的那部分代码,通过AOT编译器先静态编译了。如果AOT还没来得及编译或者不能编译,再叫醒JIT+解释器两个难兄难弟来顶住。这种机制,相当于用时间换空间,既缩短了用户安装APP的等待时间,又将虚拟机里编译器和解释器能做的优化提升到最大效率了。
很多人以为华为方舟编译器就是Android 7.0的ART虚拟机,其实不然。
无论是编译器还是解释器,只是在虚拟机上打补丁。手机上的虚拟机+编译器+解释器本身不仅占用硬件资源,还无法最大发挥软件运行性能。正因如此,所以绝大部分手机厂商只能无奈的通过简单粗暴提升安卓手机的内存和存储空间,来弥补虚拟机的弊端。
这就是安卓的第一个命门,虚拟机先天不足。
https://source.android.com/devices/tech/dalvik/