内存泄漏探索之旅(转)

内存泄漏定义:当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

最近在公司里面做某个模块的时候,使用很多静态类、方法,也有很多单例在里面,大家都知道使用静态方法、类、变量的时候,很容易造成内存泄漏问题。所以我就特意查了一下内存泄漏相关的知识,去更深层、更全面的去探讨一下这个问题。
下面是我在做相机的时候的单例工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static CameraManager mCameraManager;
private Activity mActivity;
private CameraManager(Activity activity) {
mActivity = activity;
}
public static synchronized CameraManager getInstance(Activity activity) {
if (mCameraManager == null) {
mCameraManager = new CameraManager(activity);
}
return mCameraManager;
}

单例相比大家都知道,这种写法,相比大家也经常会用,把context传进来,这样在调用方法的时候就不用传context了,是不是很节省代码量,我以前写单例的时候,也特别喜欢这么写,但是实际上这么写真的好么?
答案是错,这样写,代码量节省了,但是你可能会出现更严重的问题,内存泄漏或者功能异常。假设,我们的activity被destroy掉了, 新的activity重新开始的时候,会重新getInstance,但是我们的instance不是null的,所以我们的CameraManager不会重建,而且还是之前的activity,就会出现下面的问题:
1、如果我们使用CameraManager访问activity的时候可能发生异常,因为现在的activity不是当前的activity。
2、内存泄漏,之前activity一直被静态变量持有,所以原来的activity根本无法释放。(所以静态变量很容易造成内存泄漏的)。
这样用实例来讲,是不是很容易理解内存泄漏的定义了。

一、讲了实例之后,我们在看一下,内存泄漏,我们研究的泄漏对象是什么?

内存空间主要是静态存储区、堆区和栈区。
静态存储区:内存在程序编译的时候已经分配好了,这块内存在程序整个运行期间都存在,主要存放静态数据,全局的static数据和static常量。
栈区:执行函数时,内部变量都可以在栈上创建,函数执行结束时这些存储单元会被自动释放。栈内存在存储器的指令集中,效率高,但是分配内存有限。
堆区:动态分配内存。程序在运行的时候用malloc或new申请任意大小的内存,程序员可以在适当的时候用free或delete释放内存。

堆和栈的区别:
函数内定义的基本类型的变量和对象的引用变量都在函数的栈内存中分配。当超过变量作用域后,java就会释放掉该变量分配的内存空间,这些内存空间就可以立即被其他使用。
堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。

对于堆,频繁的new/delete会造成大量内存碎片,使程序效率降低。对于栈,它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。

最终结论:
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。属于方法的变量,生命周期随方法结束。
成员变量全部存在堆中,因为他们属于类,最终是要被new出来使用。

内存泄漏只针对于堆内存,他们存放的就是引用指向的对象实体。

二、内存为什么泄漏

内存释放是由垃圾回收机制来完成的,程序员不需要通过调用函数来释放内存,但是它只能回收无用并且不再被其他引用的对象所占的空间。

java垃圾回收机制是从程序主要运行对象开始检查,遍历一遍得到无法回收的对象和他们所引用的对象链,组成无法回收对象集合,其他孤立的对象就会被垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用
这里写图片描述

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

首先定义一个HashMap,保存软引用对象。

1
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再来定义一个方法,保存Bitmap的软引用到HashMap。
这里写图片描述

使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

回到我们的问题,为什么内存会泄露?

堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。

如果想要看一下常见的android内存泄漏问题,可以看一下这个博客:
http://blog.csdn.net/u010687392/article/details/49909477

还有一个同事总结的,性能编码规范,里面有提到过单例和static注意的地方,看了这篇文章之后,下面写的大家应该也都可以理解。
https://github.com/lizhaoxuan/Android-performance-norm