如今的Android智能手机发展信息万变,从一开始的HTC到小米价格战到如今高端市场份额战,在软硬件都发生了翻天覆地的变化。在硬件上内存从一开始的一两百M到如今4G。从软件上咱们从一开始为了实现需求而写代码到如今为了代码更健壮、更漂亮而进行不断优化代码。这些都是Android发展的必然一步。今天我来跟你们一块儿分享Android内存优化的相关概念和实践。html
进程内存与RAM之间的关系java
进程内存既是虚拟内存(或者叫逻辑内存),而程序的运行须要实实在在的内存,即物理内存(RAM),在须要的时候操做系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程可以使用物理内存。android
Android中的进程git
Google提供的Android总体架构图,能够看到Android系统是基于Linux内核的,但针对移动设备较低的内存和能耗低的需求,Android按照自身须要开发低耗的组件和库,可是Android进程
最明显的内存特征是与zygote共享内存。为了加快启动速度及节约内存,Android应用的进程都是有zygote fork出来的。因为zygote已经载入了完整的Dalvik虚拟机和Android应用框架的代码,fork出的进程和zygote共享同一块内存,这样就节约了每一个进程单独载入的时间和内存。程序员
虚拟内存分区github
虚拟内存对各类类型的数据进行存储,因为数据的杂乱,所以程序划分五个区域分别管理不一样的数据:程序寄存器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。正则表达式
Java虚拟机、Dalvik虚拟机、ART虚拟机的区别算法
虚拟机(Virtual Machine),这个名词相信你们都不陌生。说到虚拟机咱们确定要说到Dalvik和JVM。
DVM是Dalvik Virtual Machine的缩写,是安卓虚拟机的意思。(为何不叫AVM->Android Virtual Machine呢?缘由是其做者以其祖上居住过的名为Dalvik的村子命名)。
JVM是相对Java Virtual Machine而言的,对于Java(Oracle公司)与Android(Google公司)的关系你们都懂。缓存
JVM运行的是.class字节码,DVM运行的是.dex字节码格式。
Java类被编译成一个或多个字节码.class文件,打包到.jar文件中,java虚拟机从相应的.class文件和.jar文件中获取相应的字节码。
java类被编译成.class文件后,会经过一个dx工具将全部的.class文件转换成一个.dex文件,而后DVM会从其中读取指令和数据。架构
JVM基于栈,DVM基于寄存器。
JVM基于栈结构,程序在运行时虚拟机须要频繁的从栈上读取写入数据,这个过程须要更多的指令分派与内存访问次数,很耗费CPU时间。
DVM基于寄存器架构,数据的访问经过寄存器间直接传递,这样的访问方式比基于栈方式要快不少。
ART完整名称是Android Runtime。在Android5.0中,ART取代了Dalvik虚拟机(安卓在4.4中发布了ART)。
ART之因此会比Dalvik快,是由于ART执行的是本地机器指令,而Dalvik执行的是Dex字节码,经过经过解释器执行。尽管Dalvik也会对频繁执行的代码进行JIT生成本地机器指令来执行,但毕竟在应用程序运行的过程当中将Dex字节码翻译成本地机器机器指令也会影响到应用程序自己的执行,所以即便Dalvik使用了JIT,也在必定程度上也比不上直接就能够执行本地机器指令的运行时。
Android内存分配机制
Android设备出厂之后,虚拟机对单个应用的最大内存分配就肯定下来了,如dalvik.vm.heapstartsize=8m,超出这个值就会OOM。而Android为每一个进程分配内存的时候,采用了弹性的分配方式,也就是刚开始并不会一下分配不少内存给每一个进程,而是给每个进程分配一个“够用”的量。这个量是根据每个设备实际的物理内存大小来决定的。随着应用的运行,可能会发现当前的内存可能不够使用了,这时候Android又会为每一个进程分配一些额外的内存大小。可是这些额外的大小并非随意的,也是有限度的,系统不可能为每个App分配无限大小的内除。对于这个属性值是定义在/system/build.prop文件中,它配置dalvik堆的有关设定。具体设定由以下三个属性来控制:
用他们三者之间的关系作一个简单的比喻:分配dalvik heap就好像去食堂打饭,有人饭量大,要吃三碗,有人饭量小,连一碗都吃不完。若是食堂按照三碗的标准来给每一个人打饭,那绝对是铺张浪费,因此食堂的策略就是先打一碗,凑合吃,不够了本身再来加,设定堆大小也是同样,先给一个合理值,凑合用,本身不够了再跟系统要。食堂毕竟是作买卖的,若是不少人明显吃不了那么多,硬是一碗接着一碗。为了制止这种不合理的现象,食堂又定了一个策略,通常人就只能吃三碗。可是若是虎背熊腰的大汉确实有须要,能够吃上五碗,超过五碗就不给了(太亏本了)。
情景 | 值含义 |
---|---|
开始给一碗 | dalvik.vm.heapstartsize |
通常人最多吃三碗 | dalvik.vm.heapgrowthlimit |
虎背熊腰的大汉最多能吃五碗 | dalvik.vm.heapsize |
在android开发中,若是要使用大堆。须要在manifest中指定android:largeHeap为true。这样dvm heap最大可达dalvik.vm.heapsize。
RAM不足会出现的现象
当RAM不足时,Android程序主要会出现如下三种情形。
不止Android程序员,内存泄露应该是大部分程序员都遇到过的问题,能够说大部分的内存问题都是内存泄露致使的,有兴趣的同窗能够跳到个人另外连接来了解。
内存抖动
Android里内存抖动是指内存频繁地分配和回收,而频繁的gc会致使卡顿,严重时还会致使OOM。
内存溢出是一种很是严重的后果,你们能够根据接下来的分析去判断本身的程序有没作对于的优化事项。
Android应对RAM不足——内存释放垃圾回收机制
Application Framework
Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:
空进程:正常状况下,为了平衡系统总体性能,Android不保存这些进程
后台进程:存放于一个LRU缓存列表中,先杀死处于列表尾部的进程
服务进程:正常不会被杀死
可见进程:正常不会被杀死
前台进程:正常不会被杀死
Android为每个进程分配了优先级的概念,系统须要进行内存回收时最早回收空进程,而后是后台进程,以此类推最后才会回收前台进程。
Dalvik 虚拟机
上面咱们说到当Android设备出厂之后,虚拟机对单个应用的最大内存分配就肯定下来了,超出这个值就会OOM。
Android的GC操做,GC全称是Garbage Collection,也就是所谓的垃圾回收。Android系统会在适当的时机触发GC操做,一旦进行GC操做,就会将一些再也不使用的对象进行回收。
上图蓝色圈圈表示是内存中的对象,圈圈之间的箭头是对象的引用,上面的对象有的在使用,有的已经再也不使用,那GC操做会从一个叫做Roots的对象开始检查。
能够看出,黄色的对象仍然会被继续保留使用,而蓝色的对象就会在GC操做当中被系统回收掉了,这就是一次简单的垃圾回收。
虚拟机经过可达性(Reachability)来判断对象是否存活,基本思想:以”GC Roots”的对象做为起始点向下搜索,搜索造成的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达的),则该对象被断定为能够被回收的对象,反之不能被回收。
另外一方面,上文说起到Android系统会在适当的时机触发GC操做,因为Java语言的特性,不像C++等语言须要人为地进行垃圾回收,还有咱们也不须要主动去通知系统进行垃圾回收。当系统进行垃圾回收,会在打印台进行打印。打印的数据主要分为四个部分:
GC_Reason,这个是触发此次GC操做的缘由,通常状况下有如下几种缘由:
Amount_freed,表示系统经过此次GC操做释放了多少内存。
下面是一次GC操做在LogCat中打印的日志:
深刻分析垃圾回收时,咱们须要知道Android Dalvik Heap与原生Java同样,将堆的内存空间分为三个区域,Young Generation(年轻代)、Old Generation(年老代)、Permanent(读音:[ˈpɜ:rmənənt]) Generation(永久代)
最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到必定程度,它会被移动到Old Generation,最后累积必定时间再移动到Permanent Generation区域。系统会根据内存中不一样的内存数据类型分别执行不一样的gc操做。
从”GC Roots”集合开始,将内存整个遍历一次,保留全部能够被GC Roots直接或间接引用到的对象,而剩下的对象都看成垃圾对待并回收,这个算法须要中断进程内其它组件的执行而且可能产生内存碎片。
将现有的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,以后,清除正在使用的内存块中的全部对象,交换两个内存的角色,完成垃圾回收。
先须要从根节点开始对全部可达对象作一次标记,但以后,它并不简单地清理未标记的对象,而是将全部的存活对象压缩到内存的一端。以后,清理边界外全部的空间。这种方法既避免了碎片的产生,又不须要两块相同的内存空间,所以,其性价比比较高。
将全部的新建对象都放入称为年轻代的内存区域,年轻代的特色是对象会很快回收,所以,在年轻代就选择效率较高的复制算法。当一个对象通过几回回收后依然存活,对象就会被放入称为老生代的内存空间。对于新生代适用于复制算法,而对于老年代则采起标记-压缩算法。
然而复制算法和标记-压缩算法的区别在于前者是用空间换时间后者则是用时间换空间。
前者的在工做的时候是不没有独立的“mark”与“copy”阶段的,而是合在一块儿作一个动做,就叫scavenge(或evacuate,或者就叫copy)。也就是说,每发现一个此次收集中还没有访问过的活对象就直接copy到新地方,同时设置forwarding pointer。这样的工做方式就须要多一份空间。
后者在工做的时候则须要分别的mark与compact阶段,mark阶段用来发现并标记全部活的对象,而后compact阶段才移动对象来达到compact的目的。若是compact方式是sliding compaction,则在mark以后就能够按顺序一个个对象“滑动”到空间的某一侧。由于已经先遍历了整个空间里的对象图,知道全部的活对象了,因此移动的时候就能够在同一个空间内而不须要多一份空间。
结合上面咱们说的ART与DVM的比较,在【Android】揭秘 ART 细节 ---- Garbage collection文章中图文结合,十分形象地描述了ART的GC优势,你们能够自行去阅读,本文就不重复篇幅去描述了。
不要频繁的引起GC,执行GC操做的时候,任何线程的任何操做都会须要暂停,等待GC操做完成以后,其余操做才可以继续运行, 故而若是程序频繁GC, 天然会致使界面卡顿。
上文咱们介绍了不少关于内存的概念知识,在咱们了解相关后,咱们经过检测工具来发现问题.本文先介绍最多见的三款检测工具.
Android Monitor Memory
在咱们使用Android Studio时,咱们能够经过Minitor Memory来观察内存的使用状况.
了解完基本操做咱们使用运行一下项目,而后咱们手动操做GC,再必定量的操做后,看看项目的内存使用状况,此时含有正常新增的内存使用,也可能出现了内存泄漏的状况.咱们点击Dump Java Heap,等一小会儿会自动生成.hprof文件并自动弹出HPROF Viewer来分析内存使用状况.
咱们看着上图来分析,显示左上角的显示方式,有两个选项分别是Heap区域和Class List View的展现方式.
Heap类型有:
App Heap -- 当前App使用的Heap
Image Heap -- 磁盘上当前App的内存映射拷贝
Zygote Heap -- Zygote进程Heap
Class List View有:
Class List View -- 类列表方式
Package Tree View -- 根据包结构的树状显示
HPROF Viewer主要分三大模块
模块a:这个应用中全部类的名字
模块b:左边类的全部实例
模块c:在选择B中的实例后,这个实例的引用树
模块a名词解析:
名词 | 解析 |
---|---|
Class Name | 类名,Heap中的全部Class |
Total Count | 内存中该类这个对象总共的数量,有的在栈中,有的在堆中 |
Heap Count | 堆内存中这个类 对象的个数 |
Sizeof | 每一个该实例占用的内存大小 |
Shallow Size | 全部该类的实例占用的内存大小 |
Retained Size | 全部该类对象被释放掉,会释放多少内存 |
模块b名词解析:
名词 | 解析 |
---|---|
Instance | 该类的实例 |
Depth | 深度, 从任一GC Root点到该实例的最短跳数 |
Dominating Size | 该实例可支配的内存大小 |
b模块右上角有个AnalyzerTasks的按钮, 点击会进入HPROF Analyzer的hprof的分析界面,点击Analyzer Tasks右边的绿色运行箭头,Android Studio会自动的根据此hprof文件分析有哪些类是有内存泄漏的,以下图所示:
可是遇到一个常见的问题,咱们知道了这中内存泄漏的问题,可是我找到到底是哪里出现问题呀,因此接着的功能是Allocation Tracker,用来内存分配追踪。在内存图中点击途中标红的部分,启动追踪,再次点击就是中止追踪,随后自动生成一个alloc结尾的文件,这个文件就记录了此次追踪到的全部数据,而后会在右上角打开一个数据面板Allocation Tracker启动追踪.
Allocation Tracker查看方式
有两种查看方式:
Group by Allocator方式.右击直接跳到对应的代码.
点击统计图按钮,会生成上图,扇形统计图是以圆心为起点,最外层是其内存实际分配的对象,每个同心圆可能被分割成多个部分,表明了其不一样的子孙,每个同心圆表明他的一个后代,每一个分割的部分表明了某一带人有多人,你双击某个同心圆中某个分割的部分,会变成以你点击的那一代为圆心再向外展开。
MAT
接下来咱们说一下比Memory Monitor更强大的MAT。MAT全称 Eclipse Memory Analysis Tools 是一个分析Java堆数据的专业工具。
首先集成MAT工具,咱们能够在官网下载,地址:www.eclipse.org/mat/.若是你电脑有eclipse就直接安装MAT插件(不懂如何安装的同窗自行搜索"eclipse安装MAT插件").
当咱们集成了MAT工具后,咱们回到AS IDE中,以前咱们在分析Monitor Memory时候生成过.hprof文件,这时须要经过AS转换一下格式或者使用jdk自带命令转换,以下图AS转换:
接着咱们用MAT打开转换后的.hprof文件.以下图所示,咱们要关注红框内的功能.
Shallow Heap :一个对象内存的消耗大小,不包含对其余对象的引用;
Retained Heap :是shallow Heap的总和,也就是该对象被GC以后所能回收的内存大小;
详细解释可参考文章:Shallow heap & Retained heap
Histogram
可列出每个类的实例数。支持正则表达式查找,也能够计算出该类全部对象的retained size
Dominator Tree
Dominator Tree:对象之间dominator关系树。若是从GC Root到达Y的的全部path都通过X,那么咱们称X dominates Y,或者X是Y的Dominator Dominator Tree由系统中复杂的对象图计算而来。从MAT的dominator tree中能够看到占用内存最大的对象以及每一个对象的dominator。
咱们也能够右键选择Immediate Dominator”来查看某个对象的dominator。
Path to GC Roots
查看一个对象到RC Roots的引用链
一般在排查内存泄漏的时候,咱们会选择exclude all phantom/weak/soft etc.references,
意思是查看排除虚引用/弱引用/软引用等的引用链,由于被虚引用/弱引用/软引用的对象能够直接被GC给回收,咱们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump以前要手动出发GC来保证),若是有,则说明存在内存泄漏,而后再去排查具体引用。(记得看最上方的箭头,用包名来查看)
右击查看当前Object全部引用,被引用的对象:
List objects with (以Dominator Tree的方式查看)
incoming references 引用到该对象的对象
outcoming references 被该对象引用的对象
Show objects by class (以class的方式查看)
by incoming references 引用到该对象的对象
by outcoming references 被该对象引用的对象
Heap Dump Overview
从工具栏中点开 Heap Dump Overview视图,能够看到一个全局的内存占用信息
OQL(Object Query Language)
相似SQL查询语言
Classes:Table
Objects:Rows
Fileds: Cols
select * from com.example.mat.Listener
查找size=0而且未使用过的ArrayList
select * from java.util.ArrayList where size=0 and modCount=0
查找全部的Activity
select * from instanceof android.app.Activity
按红色感叹号查询结果.
上面咱们介绍了MAT几个重要的功能点.下面咱们开始来找出形成内存泄漏的凶手.
1.咱们经过Dominator Tree视图用Path to GC Roots方式来查找.
2.Object Query Language快速查找
3.内存快照对比
小结:咱们使用MAT的最终目的就是能顺利地找出内存泄漏的地方咱们上面介绍了三种方式,以前咱们经过Monitor Memory的方式已经大概能定位泄漏类,而后咱们使用MAT前两个方式十分直接地去查找到底是哪一个对象引用出现了泄漏问题.而第三个对比方式,在咱们仍是不肯定位置状况下可使用,咱们能够经过对比的方式来判断泄漏的位置.
总结:整篇文章咱们能够分为两部分,第一部分是对内存基础知识的概述.你们须要注意结构写法,是按照物理内存、虚拟机内存、Androi内存分配机制、Android内存回收机制。针对异常致使的内存泄漏、内存抖动、内存溢出咱们利用Monitor Memory,MAT,LeakCanary对内存进行了一系列的分析 其中Monitor Memory自带的内存分析工具直观方便,但其功能却不如MAT强大,特别是没有有效的搜索、排序等功能。遇到一些棘手的问题,可能仍是要借助MAT来分析内存。你们以为个人描述不够详细或准确,能够根据我这个文章描述结构造成一个基本的认识后,再根据每个小点进行细化学习。说说我本身的实操方法,我认为大型项目都要引用LeakCanary包。在debug环境下,若是出现了内存泄漏,咱们会立刻收到提示,咱们能够根据LeakCanary的信息收集地方对泄漏作一开始的判断,若是有至关的经验后,咱们能够直接修改,若是不能咱们能够经过Monitor Memory来更清晰的定位,遇到更加棘手的问题,咱们再使用MAT的方式,根据它丰富的工具一步步来定位内存泄漏的对象,并对其分析解决。不过度析了这么多,最重要的是实操。小伙伴们立刻实践起来。