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); |