各位程序猿大神们在APP开发过程中,往往由于编码不够谨慎,导致应用内存不断飙升,造成程序卡顿无响应最后异常退出。虽然Java有内存GC设计,但是人为造成的内存泄漏往往难以排查。Eclipse和Android Studio提供了如MAT工具等方式排查,但是过程比较复杂。最关键还是要在源头遏制内存泄漏的发生。文章介绍了Android的几种内存回收机制和产生内存泄漏的原因,及排查工具介绍。
一、Android 内存分配回收机制
我们知道Java内存分为栈内存(Stack)和堆内存(Heap);栈内存用来存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中;堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器(GC)来管理。
例如:
class BirthDate { private int day; private int month; private int year; public BirthDate(int d, int m, int y) { day = d; month = m; year = y; } } +++++++++++++++++我是分割线++++++++++++++++++public class Test{ public static void main(String args[]){ int date = 9; Test test = new Test(); test.change(date); BirthDate d1= new BirthDate(7,7,1970); } public void change1(int i){ i = 1234; }
栈内存/堆内存的对应方式
安卓的Application Framework也存在自己的一套内存分配回收机制。
系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。Android中由ActivityManagerService 集中管理所有进程的内存资源分配。
空进程、后台进程 、服务进程、可见进程、前台进程
前台进程:这个进程是最重要的,是最后被销毁的。前台进程是目前正在屏幕上显示的进程和一些系统进程,也就是和用户正在交互的进程。例如,我正在使用qq跟别人聊天,在我的Android手机上这个进程就应该是前台进程。
可见进程:可见进程指部分程序界面能够被用户看见,却不在前台与用户交互的进程。例如,我们在一个界面上弹出一个对话框(该对话框是一个新的Activity),那么在对话框后面的原界面是可见的,但是并没有与用户进行交互,那么原界面就是可见进程。
服务进程:服务进程是通过 startService() 方法启动的进程,但不属于前台进程和可见进程。例如,在后台播放音乐或者在后台下载就是服务进程。
后台进程:后台进程指的是目前对用户不可见的进程。例如我正在使用qq和别人聊天,这个时候qq是前台进程,但是当我点击Home键让qq界面消失的时候,这个时候它就转换成了后台进程。当内存不够的时候,可能会将后台进程回收。
空进程:空进程指的是在这些进程内部,没有任何东西在运行。保留这种进程的的唯一目的是用作缓存,以缩短该应用下次在其中运行组件所需的启动时间。
它们的回收顺序从先到后分别是:空进程,后台进程,服务进程,可见进程,前台进程。
深入到Linux内核,AMS(ActivityManagerService)会对所有进程进行评分(存放在变量adj中),然后再将这个评分更新到内核,由内核去完成真正的内存回收(lowmemorykiller, Oom_killer)。
AMS内存回收流程
Android App为什么会OOM呢?其实就是申请的内存超过了Dalvik Heap的最大值。(查看设备应用内存上限:adb shell getprop | grep dalvik.vm.heapgrowthlimit)。在分配堆内存时需要注意上限。
shell查看Heap内存
二、Android 常见内存问题
1、内存泄露
内存泄露通俗的讲就是对象不再使用后没能被回收,因为还在被其他地方持有引用。“日积月累”,轻则持续吃内存,被嫌弃,优先被杀,重则直接被杀(OOM)。
1.1 单例
因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
单例造成内存泄漏
解:全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用Activity的上下文,而是使用Application的上下文。
1.2 静态变量
静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
静态变量导致内存泄露
解:在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候将静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。
1.3 Handler
Handler也是造成内存泄露的一个重要的源头,主要Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的 ,Handler引用Activity会存在内存泄露。
Handler导致内存泄漏
解:通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。
1.4 非静态内部类创建静态实例
非静态(匿名)内部类会默认隐性引用外部类对象。
非静态(匿名)内部类
解:1.将该内部类设置为静态内部类(静态内部类不会持有外部类的引用)。
2.将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。
3.还有一种很简单粗暴的方法,就是在activity销毁的时候,将这个静态内部类设置为空。
1.5 资源使用完未关闭
BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap,资源使用后没有解注册。
解:资源使用完毕记得关闭:unregisterReceiver、unregisterContentObserver、InputStream.close、Cursor.close、Bitmap.recyle。
三、查找内存问题
1:查看运行时内存
adb shell dumsys meminfo
2:MAT工具
我们常见的还有Bitmap用完没有释放。打开Dominator Tree For Entire Heap;
MAT界面1
右击该Class Name 选择with incoming references 进去之后右击手动GC一下。看到哪里没有释放
MAT界面2
3:LeakCanary
LeakCanary是一个Android和Java的内存泄露检测库,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知,所以可以把它理解为傻瓜式的内存泄露检测工具。通过它可以大幅度减少开发中遇到的OOM问题,大大提高APP的质量。
Leakcanary界面
侵入代码仅一行,在application里添加。
LeakCanary.install(this);
这行代码的主要意图是要在你的应用application里注册Application.registerActivityLifecycleCallbacks(),这个是监控应用所有Activity生命周期的,在某个Activity onDestroy时,触发GC,遍历对象是否回收。