Android内存管理及内存泄露



Android内存管理及内存泄露,最近在看Dalvik相关的东西,发现文章不少,但许多文章都拿java虚拟机来说Dalvik,但两者是有一些不同的。

一、Dalvik的内存结构

和java虚拟机不同,Dalvik虚拟机使用的指令是基于寄存器的,而Java虚拟机使用的指令集是基于堆栈的。Dalvik虚拟机的内存大体上可以分为Java Object Heap、Bitmap Memory和Native Heap三种。

1.Java Object Heap

Java Object Heap的最小和最大默认值为2M和16M,但是手机在出厂时,厂商会根据手机的配置情况来对其进行调整,例如,G1、Droid、Nexus One和Xoom的Java Object Heap的最大值分别为16M、24M、32M 和48M。我们可以通过ActivityManager类的成员函数getMemoryClass来获得Dalvik虚拟机的Java Object Heap的最大值。

2.Bitmap Memory

Bitmap Memory也称为External Memory,它是用来处理图像的。在3.0之前,Bitmap Memory是在Native Heap中分配的,但是这部分内存同样计入Java Object Heap中,也就是说,Bitmap占用的内存和Java Object占用的内存加起来不能超过Java Object Heap的最大值。这就是为什么我们在调用BitmapFactory相关的接口来处理大图像时,会抛出一个OutOfMemoryError异常的原因。

在3.0以及更高的版本中,Bitmap Memory就直接是在Java Object Heap中分配了,这样就可以直接接受GC的管理。

3.0之前的手机,通过DDMS观看Heap信息的时候不显示native部分分配的内存大小,如图所示,加载了一张7M多的图片,但是显示分配Allocated才2M多。

3.0以后Bitmap分配的内寸在Heap上,如图

3. Native Heap

Native Heap就是在Native Code中使用malloc等分配出来的内存,这部分内存是不受Java Object Heap的大小限制的,也就是它可以自由使用,当然它是会受到系统的限制

二、dalvik虚拟机的垃圾回收

Dalvik虚拟机可以自动回收那些不再使用了的Java Object,也就是那些不再被引用了的Java Object。垃圾自动收集机制将开发者从内存问题中解放出来,极大地提高了开发效率,以及提高了程序的可维护性。

在2.3之前,Dalvik虚拟使用的垃圾收集机制有以下特点:

1. Stop-the-word,也就是垃圾收集线程在执行的时候,其它的线程都停止;

2. Full heap collection,也就是一次收集完全部的垃圾;

3. 一次垃圾收集造成的程序中止时间通常都大于100ms。

在2.3以及更高的版本中,Dalvik虚拟使用的垃圾收集机制得到了改进,如下所示:

1. Cocurrent,也就是大多数情况下,垃圾收集线程与其它线程是并发执行的;

2. Partial collection,也就是一次可能只收集一部分垃圾;

3. 一次垃圾收集造成的程序中止时间通常都小于5ms。

二、Android的内存溢出是如何发生的

Java Object Heap的最小和最大默认值为2M和16M,手机在出厂时,厂商会根据手机的配置情况来对其进行调整,我们所能利用的内存空间是有限的。如果我们的内存占用超过了一定的水平就会出现OutOfMemory的错误,从而被kill掉。


为什么会出现内存不够用的情况呢?通常是因为内存泄露引起,或者保存了多个耗用内存过大的对象(如Bitmap),造成内存超出限制。导致内存泄漏主要的原因是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,那么 这些对象就会驻留内存,消耗内存,因为无法让垃圾回收器GC验证这些对象是否不再需要。

三、常见的内存泄露引起OutOfMemory的情况

1.static静态变量

static修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的对象,就容易出现内存泄露的情况。

例1。

1
2
3
4
5
public class ClassName {
private static Context mContext;
}

以上的代码是很危险的,如果将Activity赋值到么mContext的话。那么即使该Activity已经onDestroy,但是由于仍有对象保存它的引用,因此该Activity依然不会被释放。

例2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static Drawable background;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks Test");
if (background == null) {
background= getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(background);
setContentView(label);

background是一个静态的变量,它并没有显式的保存Contex的引用,但是当Drawable与View连接之后,Drawable就将View设置为一个回调,由于View中是包含Context的引用的,所以,实际上我们依然保存了Context的引用。这个引用链如下:
Drawable->TextView->Context
所以,最终该Context也没有得到释放,发生了内存泄露。

使用static静态变量,应注意:

第一,应该尽量避免static成员变量引用资源耗费过多对象。

第二,在static变量引用对象的时候,在被引用对象不再使用的时候,应及时释放引用,做置null操作 。

2.缓存

缓存一种用来快速查找已经执行过的操作结果的数据结构,如果执行一个操作需要比较多的资源并会多次被使用,通常做法会把操作结果进行缓存,以便在下次调用该操作时使用缓存的数据,以提升应用响应速度及性能。在android应用中,比较常用的是缓存图片对象,从网络获取图片,创建一个bitmap对象都是比较耗费资源的操作,缓存图片对象,能够提升应用的体验,但是一不小心就会出现内存泄露。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ImageManager {
   private ConcurrentHashMap<String, Bitmap> mBitmapRefCache = new ConcurrentHashMap<String, Bitmap>();
   private void recycleAllBitmap() {
       for (Iterator<Map.Entry<String, Bitmap>> itr = mBitmapRefCache.entrySet().iterator(); itr.hasNext();) {
            Map.Entry<String, Bitmap> urlBitmap = itr.next();
            String url = urlBitmap.getKey();
            Bitmap bitmap = urlBitmap.getValue();
            if (bitmap != null && !bitmap.isRecycled()) {
                 bitmap.recycle();
            }
       }
   }

recycleAllBitmap方法遍历了mBitmapRefCache中缓存的所有的Bitmap对象,并进行了recycle操作,查看recycle方法的注释,recycle会释放Native Heap中占用的内存,并且告诉虚拟机,如果该图片对象没有被引用的情况下可以被回收。由于mBitmapRefCache中还有引用,gc并不会回收。

在使用缓存的适合应注意:

第一,做好缓存区的缓存对象的淘汰策略,淘汰不再使用的或者使用率较低的对象,减少缓存区的占用

第二,注意要去除被淘汰的对象的引用

第三,做好缓冲区的大小的管理,避免出现缓存对象数据太多,消耗大量内存

第四,使用SoftRefrence代替强引用

3.大对象

android应用中常见的大对象就是Bitmap,可以说出现OutOfMemory问题的绝大多数情况,都是因为Bitmap的问题。

第一、及时的回收。

虽然虚拟机垃圾回收机制会自动回收没有不再使用的Bitmap,但是由于它占用的内存过多,所以很可能会超过java堆的限制,因此,在用完Bitmap时,要及时的recycle掉,recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。

第二、设置一定的采样率。

有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:

1
2
3
4
5
private ImageView preview;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);
preview.setImageBitmap(bitmap);