背景
最近参加了华为方舟的Workshop,从编译到Runtime都有了一些体会,并且对于虚拟机的运行也有了一些了解。
Android虚拟机的演变
- 4.4版本前,使用的是Dalvik虚拟机
- 5.0版本以后,使用的是Art虚拟机
Dalvik虚拟机
原理
- Dalvik是基于寄存器的虚拟机,读取和保存数据会比基于栈的JVM在运行时快很多
- 基于寄存器的虚拟机允许更快的执行时间,但代价是编译后的程序更大
- 新的Dex字节码格式
- 合并多个class字节码文件
- 减少常量池大小
- 减少文件的IO操作,提高类的查找速度
- 减少文件大小
-
dex
的优化格式odex
- 在App安装的过程中,会通过Socket向
/system/bin/install
进程发送dex_opt
的指令,对Dex文件进行优化 - 在DexClassLoader动态加载Dex文件时,也会进行Dex的优化
- 在App安装的过程中,会通过Socket向
- Dalvik的JIT
- 在运行时对dex的指令进行intercept,解释成机器码
- 虚拟机根据函数调用的次数,来决定热点代码
- 以函数为维度将热点代码的机器码进行缓存,在下一次调用时直接调用该机器码
优点与缺点
- 优点
- 安装速度超快
- 存储空间小
- 缺点
- Multidex加载的时候会非常慢,因为在
dex
加载时会进行dexopt
- JIT中需要解释器,解释器解释的字节码会带来CPU和时间的消耗
- 由于热点代码的Monitor一直在运行,也会带来电量的损耗
- Multidex加载的时候会非常慢,因为在
5.0-7.0的Art虚拟机
在5.0-7.0(Android N)之间,Android提出了ART虚拟机的概念,而运行的文件格式也从odex
转换成了oat
格式。
原理
在APK安装的时候,PackageManagerService
会调用dex2oat
通过静态编译的方式,来将所有的dex
文件(包括Multidex)编译oat
文件。
编译完后的oat
其实是一个标准的ELF文件,只是相对于普通的ELF文件多加了oat data section
以及oat exec section
这两个段而已。
这两个段里面主要保存了两种信息:
- Dex的文件信息以及类信息
- Dex文件编译之后的机器码
在运行的时候,就直接运行oat的代码。而其中的Dex文件的内容也就是为了DexClassLoader
在动态加载其他的Dex文件时,在链接的过程中可以找到对应的meta-data,正确的链接到引用的类文件与函数。
优点与缺点
- 优点
- 运行时会超级快
- 在运行时省电,也节省各种资源
- 缺点
- 在系统更新的时候,所有app都需要进行
dex2oat
的操作,耗费的时间太长 - 在app安装的过程中,所耗费的时间也越来越长,因为apk包越来越大
- 由于
oat
文件中包含dex文件与编译后的Native Code,导致占用空间也越来越大
- 在系统更新的时候,所有app都需要进行
7.0至今的Art虚拟机
由于上述的缺点,7.0之后的采用了Hybrid Mode的ART虚拟机:
- 解释器
- JIT
- OAT
将这三种方案进行混合编译,来从运行时的性能、存储、安装、加载时间进行平衡。
在第一次启动的时候,系统会以Intercept的方式来运行App,同时启动Compilation Daemon Service
在系统空闲的时候会在后台对App进行AOT静态编译,并且会根据JIT运行时所收集的运行时函数调用的信息生成的Profile文件来进行参考 。
而根据Profile生成AOT的过程就是:Profile Guided AOT
而在JIT的过程中会进行以下事情:
- JIT的解释器:将字节码解释成机器指令
- JIT的编译器:将函数编译成机器指令
- 根据运行时的环境生成Profile文件,以供AOT编译时生成机器码
JIT的解释器
- 对字节码进行解释
- 基于计算的跳转指令
- 基于Arm汇编的Operation Code处理
- Profiling以及JIT编译的触发
- 基于函数执行次数以及搜索式的代码热度
- JIT代码缓存
- 管理编译过的缓存代码
- 为Hot Methods分配ProfilingInfo对象
JIT的编译器
函数粒度的编译
- 后台编译
- 避免Block App的UI线程
- 基于ART优化的编译器
- 使用和AOT一样的编译器
- 在优化编译器中会增强JIT的编译能力
生成Profile文件
使用单独的ProfileSaver线程
- 生成Profile文件
- 读取根据Hot Methods生成ProfilingInfo
- 把ProfilingInfo写到磁盘文件中
- 最低的消耗
- 减少Wakeup的次数
- 写入最少的信息
使用混编模式的原因
- 部分用户只使用APP的部分功能。而且这些经常使用的功能是值得被编译成Native Code的
- 使用JIT阶段找出来经常使用的代码
- 使用AOT编译以及优化来提升经常使用的这些功能
- 避免为了一些不常用的代码而付出资源(编译、存储等等)
混编模式的实现
- 在JIT的过程中,生成Offline的Profile
- Offline Profile的文件格式
- 使用AOT增强过后的编译器(dex2oat)
- 编译所使用的Daemon Service
- 只在充电或者系统IDLE的情况下才会进行AOT编译
Profile文件会在JIT运行的过程中生成:
- 每个APP都会有自己的Profile文件
- 保存在App本身的Local Storage中
- Profile会保存所调用的类以及函数的Index
- 通过
profman
工具进行分析生成
而在BackgroundDexOptService
中,会根据JIT运行时所生成的Profile以及Dex文件在后台进行AOT,根据运行时的Profile文件会选择性的将常用的函数编译成NativeCode
而整个JIT的工作流如下:
工作流华为的方舟编译器
从方舟编译器来看:
- 首先会判断该设备支不支持方舟编译器,如果支持,则从应用商店下发方舟版本的包
- 方舟编译器会把dex文件通过自己的IR翻译方舟格式的机器码,据他们说也是一个ELF文件,但是会增加一些段,猜测是Dex中类信息相关的段
- 通过这种方式,来消除Java与JNI之间的通信的损耗,以及提升运行时的效率
- 在方舟内部,还重新完善了GC算法,使得GC的频率大大降低,减少应用卡顿的现象
- 目前方舟只支持64位的So,并且对于加壳的So会出现一些问题
参考资料
Understanding JIT Compilation and Optimizations
Android profile-guided dex2oat
网友评论