Android内存优化
Android UI优化
Android性能优化 - CPU/GPU篇
Android内存优化
一、为什么要进行内存优化
在实际开发中,对对象的引用使用不当导致无法被GC,造成内存泄漏,甚至内存溢出(OOM),程序变慢,程序崩溃。
站在用户角度就是应用卡,不流畅,对应用慢慢失去兴趣。最终APP慢慢走向下线。
二、介绍Android系统内存管理机制
2.1 针对进程的内存策略
-
内存分配策略
由ActivityManagerService集中管理所有内存的分配。
ActivityManagerService简称为AMS,在Android系统中扮演很重要的角色,是系统的核心服务,主要负责系统中四大组件的启动、切换、调度和应用进程的管理、调度等工作。 -
内存回收策略
Application Framework 决定回收的进程类型,按进程优先级由低到高顺序,自动回收。
Android将进程优先级分为以下5个等级:
image.png
2.2 针对对象、变量的内存策略
-
Dalvik虚拟机与Java虚拟机对比
image.png
Android应用是运行在Dalvik虚拟机,Dalvik是Google专门设计用于Android平台的Java虚拟机,因此对象内存分配与Java相同。
-
JVM运行时数据区
image.png
区域 说明 程序计数器 每条线程都需要有一个程序计数器,计数器记录的是正在执行的指令地址,如果 正在执行的是 Natvie 方法,这个计数器值为空(Undefined) java虚拟机栈 每个方法执行的时候创建的一个栈帧用于保存局部变量表,操作数栈,动态链接,方法出口信息等。 本地方法栈 与Java虚拟机栈发挥的作用非常相似,VM 栈执行 Java 方法(字节码)服务,Native 方 法栈执行的是 Native 方法服务。 方法区 方法区是各个内存所共享的内存空间,方法区中主要存放被 JVM 加载的类信息、 常量、静态变量、即时编译后的代码等数据。 堆(Heap) 用来存储对像实例,几乎所有的对象都在这分配内存。因此垃圾回收器主要是对此进行回收,也叫GC堆。堆是运行时动态分配内存。 -
介绍JVM内存模型
image.png
内存模型中有8中原子操作,分别为:lock、unlock、read、load、use、assign、store、write。
原子操作 功能 作用对象 lock 把一个变量标识为一条线程独占状态 主内存变量 unlock 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 主内存变量 read 把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。 主内存变量 load 它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 工作内存的变量 use 把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 工作内存的变量 assign 它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 工作内存的变量 store 把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。 工作内存的变量 write 它把store操作从工作内存中一个变量的值传送到主内存的变量中。 主内存的变量
MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存中的数据,该数据就会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据变化从而将自己缓存中的数据失效。
三、Android GC机制简介
1.对象存活判断
-
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用 失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被 再使用的。
主流的JVM里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解 决对象间的互循环引用的问题。 -
可达性分析算法
通过一些列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索 所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从 GC Roots 到这个对象是不可达),则证明此对象是不可用的。所以它们会被判定 为可回收对象(例如图B中的对象既是不可达的)
image.png
Android中哪些对象可作为GC Roots呢?
1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中类静态属性引用的对象;
3.方法区中类静态属性引用的对象;
4.本地方法栈中JNI(即一般说的Native方法)引用的对象
-
Java四种引用方式
强引用、软引用、弱引用、虚引用下图为引用强弱关系和回收条件
image.png
2.垃圾回收算法
-
标记-清除算法( Mark and Sweep GC )
先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
image.png -
复制算法( Copying )
它将可用内存按容量划分为大小相等的两块,每次只使用其 中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
image.png -
标记-整理算法
先标记可回收的对象,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对 象都向一端移动,然后直接清理掉端边界以外的内存。
image.png -
分代收集算法
把 Java 堆分为新生代和老年代,这样就可以根 据各个年代的特点采用最适当的收集算法。
3.Java 堆GC角度划分
在Android中,通过new的对象都会分配到堆内存中,堆内存有分为两大块:永久空间和堆空间。
- 永久即持久代(Permanent Generation),主要存放的是Java类定义信息,与垃圾收集器要收集的Java对象关系不大。
-
Heap = {Old + NEW = {Eden,from,to}},Old即年老代(Old Generation),New即年轻代(Young Generation)。年老代和年轻代的划分对垃圾收集影响比较大。
image.png
年轻代
年轻代中分配比例大约为:Eden占8/10,from和to各占1/10。
内存分配及GC流程如下:
1.新创建的对象都会分配到Eden区;
2.当触发GC时会回收掉Eden区的对象,幸存的对象会被复制到From区,新创建的对象继续放到Eden区。
3.再次触发GC时会回收掉Eden区和From区的对象,再有幸存对象时,则全部复制到to区,继续步骤1,新建对象;
4.等到Eden区分配满,再次触发GC时,会回收掉Eden区和to区的对象,最后把幸存的对象全部复制到from区,然后继续执行步骤1;
5.如此循环往复,每次GC都会把Eden区和from或者to中的对象进行回收,并全部复制到from或to中空的那个中,每次有幸存者时计数增加1,当幸存者的技术累计到一定数量时,仍未被回收,则该对象被则被复制到老年代区;
针对年轻代的垃圾回收即Young GC。
老年代
在年轻代中经历了Ñ次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
针对年老代的垃圾回收即Full GC。
在完全GC后,若Survivor区及年老代仍然无法存放从Eden复制过来的对象,则会导致JVM无法在Eden区为新生成的对象申请内存,即出现“内存不足”。
OOM
OOM(“Out of Memory”)异常一般主要有如下2种原因:
1.年老代溢出,表现为:java.lang.OutOfMemoryError:Javaheapspace
这是最常见的情况,产生的原因可能是:内存泄漏、内存碎片等原因。
这种方式的OOM是我们本文的重点。
2.持久代溢出,表现为:java.lang.OutOfMemoryError:PermGenspace
四、常用辅助监测工具
1.Android Monitor中DDMS下的Heap Dump
Android Device Monitor工具在Android SDK目录下的tools中,点击直接运行,即可。
image.png其中总览视图可以查看整体的内存情况,表中的显示信息如下所示:
- Heap Size 堆栈分配给该应用程序的内存大小
- Allocated 已使用的内存大小
- Free 空闲的内存大小
- %Used 当前Heap的使用率(Allocated/Heap Size)
- Objects 对象的数量
Heap Dump检测内存泄漏:通常做法是使用Update Heap进行内存监听,然后操作可能发生泄漏的APP功能、界面,并点击Cause GC进行手动GC,经过多次操作后查看data object的Total Size大小是否有很大的变化,如果有则可能发生了内存泄漏,导致内存使用不断增大。
2.Android Monitor中DDMS下的Allocation Tracker
使用Heap Dump可以让你对APP的内存整体使用情况进行掌控,但缺点是无法了解每块内存具体分配给哪个对象了,这时就需要使用Allocation Tracker工具来进行内存跟踪。它允许你在执行某些操作的同时监视在何处分配对象,了解这些分配使你能够调整与这些操作相关的方法调用,以优化应用程序性能和内存使用。
Allocation Tracker能够做到如下的事情:
- 显示代码分配对象类型、大小、分配线程和堆栈跟踪的时间和位置。
- 通过重复的分配/释放模式帮助识别内存变化。
- 当与 HPROF Viewer结合使用时,可以帮助你跟踪内存泄漏。例如,如果你在堆上看到一个bitmap对象,你可以使用Allocation Tracker来找到其分配的位置。
3.MAT
想要深入的进行分析并确定内存泄漏,就要分析疑似发生内存泄漏时所生成堆存储文件。堆存储文件可以使用DDMS或者Memory Monitor来生成,输出的文件格式为hprof,然后使用MAT来分析堆存储文件。
MAT,全称为Memory Analysis Tool,是对内存进行详细分析的工具,它是Eclipse的插件,如果用Android Studio进行开发则需要单独下载它,可独立运行。下载地址
4.LeakCanary库
LeakCanary库是github上优秀的第三方开源库,通过该库可以监控内存是否有泄漏。其原理是利用对象引用可达性分析算法来进行检测的,当出现内存泄漏时会在通知栏中进行通知。
使用方式不过多介绍,请参考LeakCanary库
5.Android studio自带的Profiler工具
启动Andrroid studio,连接手机或模拟器,点击下按钮,启动要监控的应用
等应用启动后,在as底部会启动profiler分析器,点击Memory行会进入到Memory使用详情界面。
等采集完数据后,AS会自动分析Heap Dump的数据,如下图
图中筛选项说明
- app heap 指当前App使用的heap
- image heap 指磁盘上当前App的内存映射拷贝
- zygote heap zygote进程Heap(fragment占用的Heap)
我们筛选时一般按app heap进行筛选,只需要看我们自己的app堆内存分配情况。
各列信息说明
- Package Name 包名
- Allocations 分配数
- Native Size Native的大小
- Shallow Size 对象本身占用内存的大小,不包含对其他对象的引用,也就是对象头加成员变量(不是成员变量的值)的总和
-
Retained Size 对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。即该类的所有对象可支配的内存大小
image.png
针对1、2、3工具使用可参考:Android 内存检测工具
五、常见内存问题及优化方案
1.内存泄漏
文章开头也说明了内存gc机制,所谓内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,gc时一直不能被回收,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
-
单例造成的内存泄露
优化:采用Application的Context替代Activity的Context实例,若必须使用Activity,必须保证单例不能持有Activity实例 -
非静态内部类创建静态实例造成的内存泄露
优化:
a.将非静态内部类修改为静态内部类;
b.不要创建静态实例 -
Handler造成的内存泄露
优化:
1.Activity/Fragment退出时清除消息队列;
2.使用弱引用方式持有Activity/Fragment实例; -
线程造成的内存泄露
优化:
1.采用静态内部类,不持有外类引用
2.针对Activity/Fragment生命周期结束时,停止线程 -
资源使用后未关闭造成的内存泄露
优化:
文件流File读写、数据库游标Cursor、图片Bitmap使用完及时关闭和回收。 -
集合容器中的内存泄露
优化:
集合类添加集合元素对象 后,在使用后必须从集合中删除
即: 清空集合对象 & 设置为null -
WebView造成的内存泄露
优化:
将WebView放到一个单独进程中,返回WebView界面后,该进程会由系统控制回收。
2.内存抖动
App在使用过程中内存数据高低波动明显,或者界面停止不动(无任何操作)内存在不断增加、降低、再增加、再降低如此循环。
同样,举个例子简单辅助说明下:
public void click(View view) {
int id = view.getId();
if(id == R.id.btn_more_fragments){
//startActivity(new Intent(this,RadarChartActivity.class));
handler.sendEmptyMessageDelayed(100,1000);
}
}
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
for (int i = 0;i< 100;i++){
byte[] b = new byte[2048];
}
handler.sendEmptyMessageDelayed(100,100);
}
};
运行后观察内存,如下图
优化方法采用内存复用方式
将byte[] b放到循环外面作为Activity的成员变量中,优化代码如下:
//放到全局
private byte[] b;
...
for (int i = 0;i< 100;i++){
b = new byte[2048];
}
对操作大数据时,如Bitmap尽量做到内存复用,而不是频繁的new对象去分配内存空间。
3.内存碎片
内存碎片指在堆内存中存在小块的内存空间,始终得不到利用,从而造成内存空间浪费。
4KB来为内存分页中每一页的大小。
通过使用Android support V4中提供的Pools对象池,可以很好的解决内存碎片问题。