内存泄露从入门到精通三部曲之常见原因与用户实践
常见原因
1.集合类
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。在项目中的DownloadManagerTask就是这个原因。
2.单例模式
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被 JVM 正常回收,导致内存泄露。
3.Android 组件或特殊集合对象的使用
BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。其实广播忘记取消注册是特别常见的一种情况。
不要直接对 Activity 进行直接引用作为成员变量,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己声明周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。
4. Handler
要知道,只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler 的使用要尤为小心,否则将很容易导致内存泄露的发生。 有的界面会有Banner轮播图,这个时间延迟发送消息,特别容易造成内存泄漏。
5. Thread 内存泄露
线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。比如线程是 Activity 的内部类,则线程对象中保存了 Activity 的一个引用,当线程的 run 函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。这个是通常你会写一个子线程去做一些耗时操作,在界面销毁时,线程还在,因为线程很多情况下是匿内部类,所以它持有外部的一个引用。
6.一些不良代码造成的内存压力
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
6.1 Bitmap 没调用 recycle().
Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。
6.2 构造 Adapter 时,没有使用缓存的 convertView。
7. 匿名内部类
new Handler -- 循环延迟发送消息,用静态内部类和WeakReferece替代 new ICallBack -- 回调,但是回调之前方法有耗时,例如网络请求 new Thread() -- 线程,方法耗时,界面销毁没有取消 new View.OnClickListener() -- 这个超级不被注意到,如果你里面有什么耗时操作了,特别容易出错 ....
每个匿名内部类都隐式持有一个外部类的引用,所以特别容易不被发现,但是它也特别容易出错。
(包括匿名内部类) 错误的示范: public void loadData(){//隐式持有MainActivity实例。MainActivity.this.a new Thread(new Runnable() { @Override public void run() { while(true){ try { //int b=a; Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } 解决方案: 将非静态内部类修改为静态内部类。 (静态内部类不会隐式持有外部类)
注意,普通的匿名内部类可以通过显式的静态内部类来解决持有外部引用问题,上面的线程除了可以实现Runnable,然后让这个类为静态的,还有一种是把loadData变成静态方法,它里面的类自动就会变成静态匿名内部类。
8. 静态成员变量
首先,先来回顾一下static类型的变量的相关知识
只有类的成员变量才能用static修饰
方法内的局部变量和形参均不能用static修饰
static的成员变量是与类相关的,而与对象的实例无关,一个类里有一个静态成员变量,它并不影响这个类的实例被回收,即使这个类的实例被回收以后,这个静态成员变量依然存在,因为它是存在于虚拟机中的方法区中的常量区,它会随着这个虚拟机一直存在。
静态类型的变量,如果你不设置null,那么你可以认为在Application终止前,该内存永远不会回收。如果你设置了null,那么在GC时会释放。为什么不会回收? 静态对象的引用在方法区里, 方法区不参与GC。
JVM常量池:方法区的一部分,主要保存class内存结构中常量值 例如String值,public static final 类型的值。
java 静态变量生命周期(类生命周期)
加载:java虚拟机在加载类的过程中为静态变量分配内存。
类变量:static变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享
销毁:类被卸载时,静态变量被销毁,并释放内存空间。static变量的生命周期取决于类的生命周期
类初始化顺序:
静态变量、静态代码块初始化
构造函数
自定义构造函数
在Android中,类何时被卸载,其实就是ART或者DVM虚拟机重启之后。GC过程中,方法区是不参数CG的,除非你主动把这个对象设置为Null。
再来简单复习一下JVM中内存模型:
首先来了解一下jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周期中扮演着比较重要的角色:
方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
堆区:用于存放类的对象实例。
栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
除了以上四个内存区域之外,jvm中的运行时内存区域还包括本地方法栈和程序计数器,这两个区域与java类的生命周期关系不是很大,在这里就不说了,感兴趣的朋友可以自己百度一下。
程序计数器:一块较小内存区域,指向当前所执行的字节码。如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空。
本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的Native方法服务。
参考:http://www.cnblogs.com/hf-cherish/p/4970267.html
9. 不需要用的监听未移除会发生内存泄露
// tv.setOnClickListener();//监听执行完回收对象 ,这个是不会出现泄露的,只要你的实现方法里不要有耗时操作,否则它一样会因为持有外部引用而造成内存泄漏。
//add监听,放到集合里面
这种监听情况就比较严重了,因为它是把它放到一个集合里的,看下ViewPager
看到了,凡是添加函数里是addxxx或者register什么的,都要去移除。因为它其它是把它添加到一集合里,而这个监听又持有外部类的引入,是不是很扯淡。那么你也应该明白 ,为什么新版的ViewPager会用addOnPagerListener而不再是setXXX,它就是明确告诉开发都,你有add,必须要有remove。
例子2:
10. 资源未关闭引起的内存泄露情况
比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute attr.recycle()回收。 当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。
无限循环动画 没有在onDestroy中停止动画,否则Activity就会变成泄露对象。 比如:轮播图效果。
静态内部类深刻理解
为什么要用ViewHolder?
为什么要用ViewHolder,因为Making ListView Scrolling Smooth。因为如果你不用,你就要频繁的调用findViewById,它会大大的降低性能,A ViewHolder object stores each of the component views inside the tag field of the Layout, so you can immediately access them without the need to look them up repeatedly. 参考:https://developer.android.com/training/improving-layouts/smooth-scrolling.html
https://realm.io/cn/news/droidcon-farber-improving-android-app-performance/
http://androidshenanigans.blogspot.com/2015/02/viewholder-pattern-common-mistakes.html https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html
看明白了吧,每次调用findViewById就是循环自己的所有子View找到与自己的匹配的Id,然后把自己返回。
上面是findViewById的流程,循环找到当前id的View。
使用了ViewHolder可以避免多次findViewById降低查找时间 。
ViewHolder为什么要使用静态内部类?
一般来说,我们声明为静态嵌套类,当它没有依赖外部类。在我们的例子中,ViewHolder类从未引用(访问)适配器类的任何成员变量(外部类),因此我们可以声明为静态的。保持简单nested-static类只是另一个(外)类是嵌套的可读性,因为它的使用仅限于只有它的外部类。你必须宣布非静态嵌套类(称为内部类)如果访问外部类的成员变量。
《Effective Java》第22条 优先考虑静态成员类 其中有条建议: 如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。
因为非静态成员类的实例会包含一个额外的指向外围对象的引用,保存这份引用要消耗时间和空间,并且导致外围类实例符合垃圾回收时仍然被保留。如果没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例。
参考:http://www.jianshu.com/p/be247a4cf359
静态内部类与普通内部类有什么区别呢?
(1)静态内部类不持有外部类的引用
在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置所决定的),其他则不能访问。
(2)静态内部类不依赖外部类
普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生同死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
(3)普通内部类不能声明static的方法和变量
普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。
优势:
提高封装性。从代码位置上来讲,静态内部类放置在外部类内,其代码层意义就是:静态内部类是外部类的子行为或子属性,两者直接保持着一定的关系
提高代码的可读性。相关联的代码放在一起,可读性当然提高了。
形似内部,神似外部。静态内部类虽然存在于外部类内,而且编译后的类文件名也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在,也就是说我们仍然可以通过new Home()声明一个Home对象,只是需要导入“Person.Home”而已
读者可能会觉得外部类和静态内部类之间是组合关系(Composition)了,这是错误的,外部类和静态内部类之间有强关联关系,这仅仅表现在“字面”上,而深层次的抽象意义则依赖于类的设计。
http://www.jianshu.com/p/c59c199ca9fa https://gold.xitu.io/entry/569dd0537db2a20052107544(集大成的一个文章)
静态内部类和静态变量何时被回收
静态变量永远不会被销毁,除非你手动把它置为null。其实也没有被回收,因为常量区不参与GC,只是数据为Null了,不占用空间而已,不过它即使占用也是在方法区中常量池中占用。
静态内部类和普通类的被回收的规则是一样的,即不再被任何类引用。
参考文章: http://sanjay-f.github.io/categories/android/page/2/ http://sanjay-f.github.io/2016/05/28/安卓性能系列2---优化内存/ https://yq.aliyun.com/articles/3009
项目清查
ProgressWebView.java
修复后:
最后看GC引用路径的时间,其实是要看那个GCRoot是不是自己的类,如果不是,就是系统在引用,那么就无法解决,可以忽略掉。
图谱下载有大问题。
这个mMap其实一直持有多个DownloadTask,而每个DownloadTask又持有Activity,所以导致了Activity内存泄露。
UserForumAdapterUtil.java
更改:
把调用它的方法也改成非静态的。
邀请
Android自身的Bug。
这种情况特别常见,就是在应用退出后,发现有的类没有被回收,是被一些系统类给引用的,这其实是一个bug,在AndroidM被修复了。
网络请求没有取消
new ICallBack()这个接口的本质是继承一个类,对这个接口进行实现,那么它就是一个匿名内部类,匿名内部类就又回到最最容易出问题的地方了,就是它会持有外部类的引用,因为这个请求在界面销毁的时间它还没有回来,所以medicalRequest不会被回收,而ICallBack的实现又持有外部Activity,所以外部的Activity也不会回收,就会造成内存泄漏。因此,网络请求不取消这也是一个极其巨大的内存泄漏源头。
注册的recevier在界面销毁时没有取消注册。
其实在自己的项目里:
最多的就是使用了静态的成员变量,因为变量可能被静态方法引用就会把它改成静态的,还有更严重的就是静态的成员变量里是集合类型,这种更扯,因为集合类再有引用,引用内再有Context,那就是真正的大泄漏了。
还有就是,广播可能忘记注册。
最最隐藏的就是匿名内部类,一个回调、事件点击、Handler其实都是内部类,它都持有外部类的一个引用,在界面销毁的时间应该把网络请求取消掉,回调remove掉,Handler给remove掉。
Last updated