Android 内存泄漏、内存抖动和内存溢出

目录

1. 内存 2. Java GC(垃圾回收机制) 3. 定义 4. 内存泄漏原因及解决办法 4.1 外部类持有Activity的静态引用 4.2 非静态内部类和匿名内部类生命周期导致的内存泄漏 4.3 监听回调处理 4.4 资源未及时关闭 4.5 集合 4.6 static关键字修饰的变量由于生命周期过长 4.6 ThraedLocal用完及时remove 5. 内存溢出的原因 5.1 大量的图片、音频、视频处理,当在内存比较低的系统上也容易造成内存溢出 5.2 Bitmap对象的处理 5.3 间接原因——内存抖动

1. 内存

Java是在JVM所虚拟出的内存环境中运行的,Android应用层是由Java开发的,Android的davlik虚拟机与jvm也类似,只不过它是基于寄存器的。内存分为三个区:堆、栈和方法区。

栈(stack):是简单的数据结构,程序运行时系统自动分配,使用完毕后自动释放。优点:速度快。 堆(heap):用于存放由new创建的对象和数组。在堆中分配的内存,一方面由Java虚拟机自动垃圾回收器来管理,另一方面还需要程序员提供修养,防止内存泄露问题。 方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

2. Java GC(垃圾回收机制)

GC可以自动清理堆中不在使用(不在有对象持有该对象的引用)的对象。

在Java中对象如果在没有引用指向该对象,那么该对象就无从处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占据的内存。

对Android来说,内存使用尤为吃紧,最开始的app进程最大分配才8M的内存,渐渐增加到16M、32M、64M,但是和服务端相比还是很渺小的,如果对象回收不及时,很容易出现OOM内存溢出错误。

3. 定义

内存泄漏(Memory leak): 当一个对象不再使用了,本应该被垃圾回收器回收,但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的结果。这种对象存在堆内存中,就产生了内存泄漏。内存泄漏最终会导致内存溢出OOM
内存溢出(Out Of Memory):当APP占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存时就会抛出的OOM异常。也就是说程序申请的内存超出了系统能给的内存大小。
内存抖动:在短时间内有大量的对象被创建或者被回收的现象,一般是在循环中大量创建或回收对象导致的。我们在开发中应当尽量避免这种情况。

三者关系:内存抖动 < 内存泄漏< 内存溢出
内存泄漏是造成内存溢出的主要原因之一,多次内存泄漏就会导致内存溢出。
其中内存溢出最为致命,可能会导致应用程序的退出,所以解决内存溢出很重要。

4. 内存泄漏原因及解决办法

内存泄露主要表现为当Activity在finish的时候,由于对象持有对Activity的引用,造成Activity没有被及时回收。大致有以下5种情况造成内存泄露:

    static变量、匿名类的使用 线程执行处理 各种监听回调处置 Bitmap等回收处置 集合类只有增操作却没有减操作

4.1 外部类持有Activity的静态引用

原因 :单例模式造成的内存泄漏,如context的使用,单例传入的是activity的context,在activity关闭后,因为单例持有activity的引用,activity的内存无法被回收。
解决:在context的使用上,应该传入application的context到单例模式中,这样就保证了单例的生命周期跟application的生命周期一样。单例模式应该尽量少持有生命周期不同的外部对象,一旦持有该对象的时候,必须在该对象的生命周期结束前null

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TestUtil testUtil = TestUtil.getInstance(this);
    }
}
public class TestUtil {
    private static TestUtil instance;
    private Context context;
 
    private TestUtil(Context context) {
        this.context = context;
    }
 
    public static TestUtil getInstance(Context context) {
        if (instance == null) {
            instance = new TestUtil(context);
        }
        return instance;
    }
}

单例模式修改为这样,通过获取context.getApplicationContext拿到Application的context对象,保证单例中不管传入的是什么,其生命周期都是和应用一样长的,减少内存泄漏的可能性。

public class TestUtil {
	private static TestUtil instance;
	private Context context;
    
    // context.getApplicationContext
	private TestUtil(Context context) {
		this.context = context.getApplicationContext();
    }
    
    public static TestUtil getInstance(Context context) {
        if (instance != null) {
            instance = new TestUtil(context);
        }
        return instance;
    }
}

4.2 非静态内部类和匿名内部类生命周期导致的内存泄漏

原因:当activity中创建的非静态内部类或者匿名内部类会持有Activity的隐式引用,Handler、Thread、AsyncTask、Runnable等,若内部类生命周期长于Activity(也就是当activity退出finish,但是内部类的异步任务尚未执行完成),会导致Activity实例无法被回收,造成内存泄漏。(屏幕旋转后会重新创建Activity实例,如果内部类持有引用,将会导致旋转前的实例无法被回收)

 public class MainActivity extends AppCompatActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new AsyncTask<String, Void, String>() {
            @Override
            protected String doInBackground(String... params) {
                for (int i = 0; i < 15; i++) {
                    try {
                        Log.e("MainActivity2", "dddd" + i + MainActivity2.this.getLocalClassName());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return null;
            }
 
            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
            }
        }.execute();
    }
 }

在上面的代码中,存在一个非静态的匿名类对象AsyncTask,会隐式持有一个外部类的引用MainActivity 。同理,若是这个AsyncTask作为MainActivity的内部类而不是匿名内部类,他同样会持有外部类的引用。当MainActivity在这15秒内退出或者重建,AsyncTask异步任务尚未处理完成,会持有activity引用,导致其无法回收造成内存泄漏。

解决:如果一定要使用内部类,就改用static静态内部类,在内部类中通过WeakReference的方式引用外界资源。对Handler、Thread、Runnable等使用弱引用;或者在activity生命周期结束前调用removeCallbacksAndMessages等移除异步任务,这样没有引用了自然可以回收。

举例:使用静态的Handler内部类,并通过WeakReference弱引用

public class MainActivity extends AppCompatActivity {
	static MyHandler myHandler;
	
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        myHandler = new MyHandler(this);
    }
    static class MyHandler extends Handler {
        WeakReference<Activity> mActivityReference;
 
        MyHandler(Activity activity) {
            mActivityReference = new WeakReference<Activity>(activity);
        }
 
        @Override
        public void handleMessage(Message msg) {
            final Activity activity = mActivityReference.get();
            if (activity != null) {
                //....
            }
        }
    }
}

4.3 监听回调处理

在onPause()/onDestroy()方法中解除监听器,包括在Android自己的Listener,Location Service或Display Manager Service以及自己写的Listener。

4.4 资源未及时关闭

原因:对于BraodcastReceiver,ContentObserver,Cursor,File,Stream,ContentProvider,Bitmap,动画,I/O,数据库,网络的连接等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

解决

广播BraodcastReceiver:记得注销注册unregisterReceiver(); 文件流File:记得关闭流InputStream / OutputStream.close(); 数据库游标Cursor:使用后关闭游标cursor.close(); 对于图片资源Bitmap:当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存,再赋为null 动画:属性动画或循环动画,在Activity退出时需要停止动画。在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy中没有去停止动画,那么这个动画将会一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。在Activity中onDestroy去调用objectAnimator.cancel()来停止动画。
public class TestActivity extends AppCompatActivity {
    private TextView textView;
    ObjectAnimator objectAnimator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        textView = (TextView) findViewById(R.id.textview);
        objectAnimator = ObjectAnimator.ofFloat(textView, "rotation", 0, 360);
        objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
        objectAnimator.start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 停止动画,防止内存泄漏
        objectAnimator.cancel();
    }
}

4.5 集合

集合对象及时清理,使得JVM回收。我们通常会把对象存入集合中,当不使用时,清空集合,让相关对象不再被引用。

 objectList.clear();
 objectList = null;

4.6 static关键字修饰的变量由于生命周期过长

原因:静态变量Activity和View会导致内存泄漏。例如下面代码中的静态变量context,textView的生命周期与应用的生命周期一样,而他们都持有当前Activity的(TestActivity )引用,一旦TestActivity 销毁,而它的引用一直被持有,就不会被回收,内存泄漏就产生了。
解决:static对象的生命周期过长,应该谨慎使用,一定要使用要及时进行null处理。

 public class TestActivity extends AppCompatActivity{   
 	private static Context context;    
 	private static TextView textView;  

 	@Override    
 	protected void onCreate(Bundle savedInstanceState){   
     	super.onCreate(savedInstanceState);    
     	setContentView(R.layout.activity_main);    
     	context = this;    
     	textView = new TextView(this);
     }    
 }

4.6 ThraedLocal用完及时remove

原因:我们都知道,threadlocal中维护了一个ThreadLocalMap,而threadlocal作为map的值弱引用存在,当threadlocal为null的时候,垃圾回收机制回收,但是此时我们的threadlocalmap与thread生命周期一致,不会被回收,ThreadLocalMap的key没了,但是value还在,这样就会导致内存泄漏。
解决:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

ThreadLoacal

5. 内存溢出的原因

产生内存溢出的原因主要有:

    内存泄漏,上面讲了内存泄漏产生的原因及解决,在程序开发过程中应尽量避免 保存了多个耗用内存过大的对象(如Bitmap)或加载单个超大的图片,造成内存超出限制

5.1 大量的图片、音频、视频处理,当在内存比较低的系统上也容易造成内存溢出

建议使用第三方,或者JNI来进行处理;

5.2 Bitmap对象的处理

不要在主线程中处理图片 使用Bitmap对象要用recycle释放 控制图片的大小,压缩大图,高效处理,加载合适属性的图片。
当我们有些场景是可以显示缩略图的时候,就不要调用网络请求加载大图,例如在RecyclerView中,我们在上下滑动的时候,就不要去调用网络请求,当监听到滑动结束的时候,才去加载大图,以免上下滑动的时候产生卡顿现象。
 // Bitmap对象没有被回收
 if (!bitmapObject.isRecyled()) {
     // 释放  
     bitmapObject.recycle(); 
     // 提醒系统及时回收 
     System.gc(); 
 }  

5.3 间接原因——内存抖动

当频繁大量的创建/回收对象的时候,会造成内存抖动,会导致GC处理机制频繁的运行。
因为new对象会在内存中引用大量内存地址,如果对象大量并且没有被回收的时候,当需要大片的连续地址时,内存中空间足够,但是提供不了连续的内存,也会导致内存溢出。

来源url
栏目