文章目录
要学习Android的内存优化,首先要了解Java虚拟机,此前我用了多篇文章来介绍Java虚拟机的知识,就是为了这个系列做铺垫。在Android开发中我们接触的是与Java虚拟机类似的Dalvik虚拟机和ART虚拟机
一、android的系统框架
一 应用程序层
该层提供一些核心应用程序包,例如电子邮件、短信、日历、地图、浏览器和联系人管理等。同时,开发者可以利用Java语言设计和编写属于自己的应用程序,而这些程序与那些核心应用程序彼此平等、友好共处。
二 应用程序框架层
该层是Android应用开发的基础,开发人员大部分情况是在和她打交道。应用程序框架层包括活动管理器、窗口管理器、内容提供者、视图系统、包管理器、电话管理器、资源管理器、位置管理器、通知管理器和XMPP服务十个部分。在Android平台上,开发人员可以完全访问核心应用程序所使用的API框架。并且,任何一个应用程序都可以发布自身的功能模块,而其他应用程序则可以使用这些已发布的功能模块。基于这样的重用机制,用户就可以方便地替换平台本身的各种应用程序组件。
三 系统库和Android运行时
系统库包括九个子系统,分别是图层管理、媒体库、SQLite、OpenGLEState、FreeType、WebKit、SGL、SSL和libc。Android运行时包括核心库和Dalvik虚拟机,前者既兼容了大多数Java语言所需要调用的功能函数,又包括了Android的核心库,比如android.os、android.net、android.media等等。后者是一种基于寄存器的java虚拟机,Dalvik虚拟机主要是完成对生命周期的管理、堆栈的管理、线程的管理、安全和异常的管理以及垃圾回收等重要功能。
四 Linux内核
Android核心系统服务依赖于Linux2.6内核,如安全性、内存管理、进程管理、网络协议栈和驱动模型。Linux内核也是作为硬件与软件栈的抽象层。驱动:显示驱动、摄像头驱动、键盘驱动、WiFi驱动、Audio驱动、flash内存驱动、Binder(IPC)驱动、电源管理等。
二、Dalvik虚拟机
Dalvik虚拟机是Android程序的虚拟机,是Android中Java程序的运行基础。其指令集基于寄存器架构,执行其特有的文件格式——dex字节码来完成对象生命周期管理、堆栈管理、线程管理、安全异常管理、垃圾回收等重要功能。
它的核心内容是实现库(libdvm.so),大体由C语言实现。依赖于Linux内核的一部分功能——线程机制、内存管理机制,能高效使用内存,并在低速CPU上表现出的高性能。每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,其代码在虚拟机的解释下得以执行。
优势:
- 在编译时提前优化代码而不是等到运行时
- 虚拟机很小,使用的空间也小;被设计来满足可高效运行多种虚拟机实例。
- 常量池已被修改为只使用32位的索引,以 简化解释器**
2.1 Dalvik VM ≠Java VM
DVM之所以不是一个JVM ,主要原因是DVM并没有遵循JVM规范来实现。DVM与JVM主要有以下区别
2.1.1 基于的架构不同
栈结构
- 栈是线程独有的,保存其运行状态和局部自动变量的(所以多线程中局部变量都是相互独立的,不同于类变量)。
- 栈在线程开始的时候初始化(线程的Start方法,初始化分配栈),每个线程的栈互相独立。
- 每个函数都有自己的栈,栈被用来在函数之间传递参数。
- 操作系统在切换线程的时候会自动的切换栈
计算20+7在栈中的计算过程:
1、POP 20
2、POP 7
3、ADD 20, 7, result
4、PUSH result
JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度慢,对于性能有限的移动设备,显然不是很适合。
寄存器结构
- 寄存器是中央处理器内的组成部分。
- 寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。
- 在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC),在中央处理器的算术及逻辑部件中,包含的寄存器有累加器(ACC)
基于寄存器的虚拟机,它们的操作数是存放在CPU的寄存器的。没有入栈和出栈的操作和概念。但是执行的指令就需要包含操作数的地址了,也就是说,指令必须明确的包含操作数的地址
//需要明确的制定操作数R1、R2、R3(这些都是寄存器)的地址
ADD R1, R2, R3 ;就一条指令搞定了。
DVM是基于寄存器的,它没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈指令,同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。
2.1.2 执行的字节码不同
在Java SE程序中,Java类会被编译成一个或多个.class文件,打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为: .java文件 -> .class文件 -> .jar文件
而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该.dex文件读取指令和数据。执行顺序为:
.java文件 –>.class文件-> .dex文件
如上图所示,.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。
而在.apk文件中只包含了一个.dex文件,这个.dex文件里面将所有的.class里面所包含的信息全部整合在一起了,这样再加载就提高了速度。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中,减少了I/O操作,提高了类的查找速度。
dex文件是虚拟机直接执行的文件。但是系统做了一些优化,不直接从apk中提取dex运行启动apk,效率太慢,所以就有了后面的odex、oat文件,我们后续再说
2.1.3 DVM允许在有限的内存中同时运行多个进程
DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
2.1.4 DVM由Zygote创建和初始化
在Android系统启动流程解析Zygote进程启动过程这篇文章中我介绍过 Zygote,可以称它为孵化器,它是一个DVM进程,同时它也用来创建和初始化DVM实例。每当系统需要创建一个应用程序时,Zygote就会fock自身,快速的创建和初始化一个DVM实例,用于应用程序的运行。
2.2 DVM架构
DVM的源码位于dalvik/目录下,其中dalvik/vm目录下的内容是DVM的具体实现部分,它会被编译成libdvm.so;dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump是.dex文件的反编译工具;DVM的可执行程序位于dalvik/dalvikvm中,将会被编译成dalvikvm可执行程序。DVM架构如下图所示。
从上图可以看出,首先Java编译器编译的.class文件经过DX工具转换为.dex文件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行,最后交与Linux处理。
2.3 Optimized DEX,即优化过的DEX
- odex
- 5.0之前,在使用DVM虚拟机的时候,系统设计的文件,odex是由classess.dex生成的,即优化的过dex
- odex怎么生成的
-(5.0之前)apk在安装的时候,就会验证和优化,验证代码合法性和代码执行速度。验证完毕后会产生odex - odex的特点什么作用?
- 运行apk的时候,直接加载odex,避免重复验证和优化,加快了apk的响应时间,加快软件加载速度
- 防盗版,因为在安装apk的时候,系统会把apk压缩包里面的dex文件删除,并把apk文件存放到权限比较高的目录,一般用户拿不到,即使拿到也用不了,因为里面的dex文件已经被删除了
2.4 Dalvik虚拟机你需要知道的15个问题
- 大部分jvm是基于栈的,而Dalvik是基于寄存器的。
基于栈的机器必须使用指令来载入栈上数据,或是用指令来操纵数据,因此指令集更为庞大。但是对于寄存器指令而言,又必须指定源地址和目的地址,因此,基于寄存器的jvm单个指令更大。 - Dalvik一些特点:
a)常量池32位索引
b)默认栈12kb,3个页,每页4kb
c)默认启动堆2MB,最大值16MB,最小1MB
d)堆最大支持1024MB
e)堆和栈的参数可以通过-Xms和-Xmx更改 - 所有的android线程都对应一个linux线程。每个Android Dalvik应用程序都运行在自己的沙盒里,不同的应用在不同的进程空间里运行。
- Dalvik相当于java的JVM,.NET的CLI,Python、Perl、Ruby的Interpreter。Dalvik定义自己的字节码为VM的指令。
- 目前Dalvik支持的功能:
a).dex文件
b)Dalvik指令集
c)J2ME CLDC API
d)多线程 - Dalvik支持的平台有:
a)基于Unix的系统
b)Linux
c)BSD
d)Mac OSX - Dalvik 虚拟机实现位于 dalvik/目录下,dalvik/vm是虚拟机的实现部分,被编译为libdvm.so,而dalvik/libdex被编译成libdex.a静态库作为dex工具库;dalvik/dexdump是.dex文件的反编译工具。虚拟机的可执行程序位于dalvik/dalvikvm中,将被编译为dalvikvm可执行程序。
- Dalvik需要的其他库:
a)OpenSSl 加密技术
b)Zlib 免费的一般目的数据压缩库
c)ICU 字符编码技术
d)java包 包括java.nio,java.lang,java.util
e)Apache Harmony classlibApache HttpClient - Dalvik虚拟机的运行库大部分是用可移植的C写的,除了JNI call bridge。
- Dalvik不遵循java SE和java ME的API规范,所以不支持AWT或者Swing。
- dalvik/vm/Dvm.mk 中会根据dvm_arch来选择编译的目标集体系结构。
- dx工具:位于dalvik/dx目录,用于将字节码转换成.dex。
例:dx --dex --output=helloworld.dex helloworld.class - dexdump工具:位于dalvik/dexdump目录,用于反编译dex文件。
- dex数据类型:
byte 8bit
ubyte 8bit
short 16bitlittle-endian
ushort 16bit little-endian
int 32bitlittle-endian
uint 32bitlittle-endian
long 64bitlittle-endian
ulong 64bitlittle-endian
sleb128 LEB128 variable-lengtha
uleb128 LEB128 variable-lengtha
uleb128p1 LEB128 variable-lengtha
LEB128类型:1~5个字节组成。所有字节组合在一起代表一个32位值。除最后一个字节最高标志位为0外,其他都为1,剩下的7位为有效负荷。有符号的LEB128的符号由最后一个字节的有效负荷最高位决定。具体算法在:dalvik/libdex/LEB128.h。 - dex文件被映射到DexMapList,结构体定义在dalvik/libdex/DexFile.h© 中。
2.4 DVM的运行时堆
DVM的运行时堆主要由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。
- Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,所有进程都共享该区域,比如系统资源。
- Allocation Space是在Zygote进程fork第一个子进程之前创建的,它是一种私有进程,Zygote进程和fock的子进程在Allocation Space上进行对象分配和释放。
除了这两个Space,还包含以下数据结构:
- Card Table:用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。
- Heap Bitmap:有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。
- Mark Stack:DVM的运行时堆使用标记-清除(Mark-Sweep)算法进行GC,不了解标记-清除算法的同学查看Java虚拟机(四)垃圾收集算法这篇文章。Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。
三、ART虚拟机
- Android 2.2 使用DVM虚拟机
- Android 4.4 推出ART虚拟机
- Android 5.0 全面替换ART
- Android7.0 仍是ART环境,但是编译策咯改为AOT 和 JIT 混合编译
- 首次安装 不再统一执行dex2oat
- 改由程序运行时根据实际情况来决定哪些部分被编译成本地代码,会采用JIT混合编译
- 然后在满足两个条件的时候在执行AOT优化。
- 集合了AOP和JIT的特点,使得安装速度,运行速度,储存空间和耗电等指标都得到了优化
ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ART。在Android 5.0时,默认采用ART,DVM从此退出历史舞台。
3.1 ART与DVM的区别
DVM
- 安装快,体积小,但是每次运行APP都要编译, 每次编译特别耗电
- DVM中的应用每次运行时,字节码都需要通过即时编译器(JIT,just in time)转换为机器码,这会使得应用的运行效率降低。
- 基于Dex,但是安装时会转为ODex加速使用,Odex会放置在系统目录下,放置篡改
ART
- 安装体积大慢,但是每次运行APP直接读取本地机器码效率快,不用每次编译比较省电
- 在ART中,系统在安装应用时会进行一次预编译(AOT,ahead of time),将字节码预先编译成机器码并存储在本地,这样应用每次运行时就不需要执行编译了,运行效率也大大提升
- 基于Dex,但是会转为OA
3.2 ART的运行时堆
与DVM的GC不同的是,ART的GC类型有多种,主要分为Mark-Sweep GC和Compacting GC。ART的运行时堆的空间根据不同的GC类型也有着不同的划分,如果采用的是Mark-Sweep GC,运行时堆主要是由四个Space和多个辅助数据结构组成,四个Space分别是Zygote Space、Allocation Space、Image Space和Large Object Space。Zygote Space、Allocation Space和DVM中的作用是一样的。Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12k)。其中Zygote Space和Image Space是进程间共享的。
采用Mark-Sweep GC的运行时堆空间划分如下图所示。
除了这四个Space,ART的Java堆中还包括两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map,以及三个Object Stack。如果想要跟多的了解它们,请参考ART运行时Java堆创建过程分析 – 罗升阳这篇文章。
3.3 OAT & ART
5.0全面替换ART虚拟机,使用AOT编译模式,dex文件经过dex2oat编译,会生成.art、.oat两个文件
-
oat
- 一个android定制的elf文件,原始dex也保存在其中。8.0后,dex单独保存到.vdex文件中
- 比dex文件大10%-20%(因为它包括dex和本地代码机器指令)
- ART安装慢,体积大,多余10%-20%, 但是每次运行APP直接读取本地机器码,效率快,不用每次编译 比较省电
-
art文件
- 类似于一个内存映像,缓存常用的ArtField、ArtMethod、DexCache等内容,加载后可直接使用,避免解析耗时
- odex进行优化生成的可执行二进制码文件
- 用于加快应用启动速度
- 从odex中拆分出来的,art文件主要为了加快应用的对“热代码”的加载与缓存
3.3.1 vdex
google在android8.0新增加了vdex文件,通过package直接转化的可执行二进制文件。
- 加快验证速度的元数据,在手机安装APK的时候,系统都会验证代码的合法性。(vdex会较快验证速度)
- 有助于提升软件更新的性能和体验。(vdex会保存验证过的dex文件,以便在ART系统更新期间,无需再次解压验证dex)
- 默认是启动状态,停用该功能 请将 ART_ENABLE_VDEX 环境变量设为 false。
四、 smali文件
虚拟机有自己的一套指令集,汇编语言,反编译dex文件就可以得到smali文件, smali文件就是寄存器语言
- smali文件就是寄存器语言
- DVM ==> java ==>.class ==>.dex ==> odex == >smali
- ART == > java ==>.class ==>.dex ==> oat == >smali
- 无论的dex,odex,vdex,oat他们只不过是对dex的优化,最终加载在虚拟机后,会转换成smali寄存器语言,去运行