最近又碰到了一个OOM的问题,头疼...这次还是好好总结下,免得下次抓虾。
1. 内存泄露
2. Android里的垃圾回收
3. Heap
4. 调试
5. 常见的内存泄露
1.什么是内存泄露(memory leak)?
A "memory leak" in your code is when you keep a reference to an object that is no longer needed。许多小白,类似我,会问,java不是有自己的垃圾回收机制吗,为什么还会发生内存泄露那?
2.Android里的垃圾回收?
垃圾回收是JAVA里的一个特性,正如我们所知,垃圾回收将会回收所有不能被GC root object所触及的对象。OOM的分析起点都是Garbage Collection Roots。任何可以通过GC root到达的对象会继续留在内存中,而那些通过GC root不能到达的对象则会被回收。比如下图,红色的是GC root,蓝色圆圈代表可以通过GC root到达的对象,黑色的就是会被GC回收的。蓝色的是留在内存中的,也就是OOM的发生点。
图1
GC root object包括:
(1)在当前调用栈里的对象,包括本地变量和方法参数
(2)JNI native references
(3)所有被引导程序(bootstrap Loader)所加载的类
(4)进程本身
在gingerbread之前,垃圾回收采用的是“stop the world”的策略,它的意思是当进行垃圾回收的时候,所有程序都必须停掉,进行整个Heap的回收;从gingerbread开始,垃圾回收采用的是同步回收机制,部分回收,暂停时间通常小于5ms.
举个例子,bitmap在honeycomb之前程序通过recycle(), finalizer进行回收,垃圾回收采用的事整个heap的回收,“stop the world”。在honeycomb之后,系统对bitmap的回收进行的同步的回收。
3.Heap
dalvik.vm.heapsize决定了Heap的大小。在gingerbread里,android引进了一项新的android:largeHeap,但是用这个选项的时候要慎重,是不是你的程序的确需要这么大的Heap?因为大Heap意味着的是更长时间的垃圾回收。
几个重要概念
Shallow heap:某个对象所消耗掉的内存。Retained set of X: 当X垃圾回收进行时,一系列将被GC删除的对象集合。Retained heap of X:X保持的活跃内存。一个对象的Shallow heap是它在heap中的大小;而它的Retained heap指的是当这个对象进行垃圾回收的时候,heap中可以被释放掉的内存大小。比如下图2中,黄色原点的Shallow heap是100,Retained heap是400.
图2
Dominators:如果对象A统治着对象B,意味为通往B的每条路都要经过A。GC会回收所有没有被引用的对象。比如A在内存中被删除了,再也没有任何路可以通往B了,这个时候B就会被GC回收。
Dominator tree:用Dominators理念组织内存中对象的关系就是Dominator tree。tree的每个节点存储了可以被释放的内存。从这个树中你可以看出最大的一块回收内存和各个对象中依然保持活跃的依赖关系。
Reference对象:
Strong reference | 从不被GC回收的 Object o=new Object(); Object o1=o; |
Soft reference | 正常情况下不被GC回收,只在具有明显内存操作要求情况下被回收。 Memory-sensitive caches |
Weak reference | 可被GC回收。Weak reference是一种不够强大的reference,以至于它不能使对象强行留在内存中。如果一个对象只是被Weak referenced,意味着它随时可能被GC回收。 |
4.内存泄露调试
OOM和其他BUG不太一样的地方在于,LOG可能一点用处都没有。在发送OOM的时刻,整个系统处于内存饱和状态,某个程序挂掉是很正常的事情,所以LOG也没有什么太大的参考性,最重要的是hprof文件。它反映了这个时刻,内存的使用状况。我用Eclipse的MAT来分析hprof。
首先,Android手机里的hprof要转换成Eclipse的能识别的格式。android SDK自带有这个转换工具,在SDK的tools目录下输入这个命令,将手机中的outofmemory0.hprof转换成Eclipse可识别的new.hprof。
hprof-conv outofmemory0.hprof /Share/new.hprof
然后将这个导入Eclipse。MAT会给出可疑点分析,列出dominate tree,Histogram view等等,你可以看到某个对象有多少个,占用了多少内存,到底是谁一直持有它等等。是非常有用的一个工具。MAT的帮助文档写的也非常清楚,大家可以去看下。
5.常见的内存泄露
(1)持有对象不释放。
这个就是我最近碰到的一个非常典型的OOM:Activity A的onCreate()里, B b = new B(mContext);在B的构造方法里,注册了一个监听;但是在Activity A的onDestroy()并没有反注册这个监听,导致这个监听一直存在。所以这个监听一直持有mContext,从hprof文件中看,存在有7个Activiy A的实例。
换句话说,Activity A的引用并没有和A的生命周期保持一致。在A被销毁后,A的引用却依然存活。
最后的解决方法是,用application context替换了Activity context,这个也是谷歌提倡的避免OOM的一个方法。
为啥那?可能我的理解不是很到位:因为理论上application只有一个,而activity可以有多个。application context保证了在整个application的生命周期里,对application的引用只有这一个。
(2)Cursor不关闭。
这个是最常见的操作数据库导致的内存泄露。在数据量较小的并且操作次数不多的情况下,系统是没有什么特别的表现的。但是一旦数据量和操作次数变多,内存泄露就会出现。所以Cursor在使用过后一定要记得关闭。这个应该算是常识,但是在真实项目中的确会碰到由这个原因导致的问题。
(3)图片问题
(4)非静态内部类问题
(3)(4)回头更新,经验尚浅,的确没碰见过。
OOM依然是很复杂的问题,估计自己连皮毛都没搞懂,总结的这个也有很多错误,不过今后会不断更新完善。经历这次OOM,就一个感觉,一切你畏惧的东西都是纸老虎。下面几个link写的很好,留下来反复看。ps为什么人家写的技术就那么有逻辑那么清楚那?
http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html
http://kohlerm.blogspot.com/2009/07/eclipse-memory-analyzer-10-useful.html
http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html