Android之内存泄露、内存溢出、内存抖动

内存

JAVA 是在JVM所虚拟出的内存环境下运行的,内存分为三个区:堆、栈和方法区。

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

GC可以自动清理堆中不在使用(不在有对象持有该对象的引用)的对象。
在Java中的对象如果没有引用指向该对象,那么该对象就无法处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占的内存。
对于Android 来说,内存使用尤为紧张,最开始的app进程最大分配才8M的内存,渐渐增加到16M、32M、64M,但相比服务器还是很小的。所以如果对象回收不及时,很容易出现OOM异常。

Java中的引用
  • 强引用:使用最普遍的引用;如果一个对象具有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,让程序异常终止,也不会随意回收具有强引用的对象来解决内存不足的问题。
  • 软引用:如果一个对象只具有软引用,内存空间足够时,垃圾回收器不会回收它,当虚拟机报告内存不够才会回收。只要垃圾回收器没有回收它,该对象就可以被程序使用。软应用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用:只具有弱引用的对象拥有更短的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现哪些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  • 虚引用:虚引用可以理解为虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。

虚引用与(软引用弱引用)的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

内存泄露、内存溢出、内存抖动
名称 概念
内存溢出
(Out of Memory)
系统会给每个APP 分配内存也就是Heap Size值。当APP占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存是就会抛出Out Of Memory异常。
内存泄漏
(Memory Leak)
当一个对象不在使用了,本应该被垃圾回收器回收,但这个对象由于被其它正在使用的对象所持有,造成无法被回收的结果。内存泄漏最终会导致内存溢出。
内存抖动 内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中有大量创建、回收对象。这种情况应当尽量避免。

它们三者的重要等级为:内存溢出 > 内存泄漏 > 内存抖动。

内存溢出会触发Java.lang.OutOfMemoryError,造成程序崩溃。

内存泄漏是造成应用程序OOM的主要原因之一。由于Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免导致应用所需要的内存超过系统分配的内存限额,就造成内存溢出导致应用Crash。
APP多次出现内存泄漏,可能会导致内存溢出。但是,APP出现内存溢出,不一定是因为内存泄漏,因为Android系统本身分配给每个APP的空间就那么一点。另外,内存泄漏也不一定就会出现内存溢出,因为还可能泄漏的速度比较慢,系统将进程杀死了,也就不会内存溢出。不过,发现内存泄漏,还是要第一时间解决。

处理方式汇总
1. 释放强引用,使用软引用和弱引用
2. 大量的图片、音乐、视频处理,当在内存比较低的系统上造成内存溢出,建议使用第三方,或者JNI来进行处理。
3. Bitmap对象的处理
  • 不在主线程中处理图片
  • 使用Bitmap对象要用recycle释放
  • 控制图片的大小,压缩大图,高效处理,加载合适属性的图片
4. 非静态内部类和匿名内部类Handler、Thread、Runnable等由于持有外部类Activity的引用,从而关闭Activity,线程未完成造成内存泄漏
  • 在Activity中创建的非静态内部类,会持有Activity的隐式引用,若内部类生命周期长于Activity,会导致Activity实例无法被回收。(例如:屏幕旋转后会重新创建Activity实例,如果内部类持有引用,将会导致旋转前的实例无法被回收)。
  • 如果一定要使用内部类,就改用static内部类,在内部类中通过WeakReference的方式引用外界资源。对Handler、Thread、Runnable等使用弱引用,并且调用removeCallbackAndMessage等移除。
    例如:下面的代码中存在一个非静态的匿名内部类对象Thread,会隐式持有一个外部类的引用MainActivity。同理,若是这个Thread作为MainActivity的内部类而不是匿名内部类,同样会持有外部类的引用。
public class MainActivity extends AppCompatActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
         threadFun();
     }
 
     private void threadFun() {
         new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     Thread.sleep(10 * 1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
     }
 }

在线程休眠的10s内,会一直隐式持有外部类的引用MainActivity,如果在10s内销毁MainActivity,就会报内存泄漏。同理,若是这个Thread作为MainActivity的内部类而不是匿名内部类,也会内存泄漏。所以如果Activity在销毁之前任务还未完成,就会导致Activity的内存资源无法回收,造成内存泄漏。
解决方法: 这里只需要将Thread匿名内部类定义成静态的内部类即可(静态内部类不会持有外部类的一个隐式引用)。或者保证Activity在销毁之前完成任务。

  • 在关闭Activity的时候停掉后台线程。线程停了,就相当于切断Headler和外部连接的线,Activity自然会在合适的时候被回收。
5. 资源未及时关闭造成的内存泄漏

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

  • 广播BroadcastReceiver:记得注销注册unregisterReceiver()
  • 文件流File:记得关闭流InputStream / OutputStream.close()
  • 数据库游标Cursor:使用后关闭游标cursor.close()
  • 对于图片资源Bitmap:当它不在被使用时,用recycle()回收此对象的像素所占用的内存,在赋值为null
  • 动画:属性动画或循环动画,在Activity退出是需要停止动画。在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy()中没有去停止动画,那么这个动画将一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。在Activity的onDestroy()去调用objectAnimator.cancel()来停止动画。
  • 集合对象及时清理,使得JVM回收:通常把对象存入集合中,当不使用时,清空集合,让相关对象不在被引用
6. 注销监听器

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

7. static关键字修饰的变量由于生命周期过长,容易造成内存泄漏
  • static对象的生命周期过长,应该谨慎使用。一定要使用要及时进行null处理
  • 静态变量Activity和View会导致内存泄漏。例如:context,textView实例的生命周期与应用的生命周期一样,而它们都持有当前Activity的(MainActivity)引用,一旦MainActivity销毁,而它的引用一直被持有,就不会被回收。所以,内存泄漏就产生了。
public class MainActivity 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);
 	}    
 }

8. 如果使用Context,尽可能使用Application的Context
  • 单例模式造成的内存泄漏,如context的使用,单例中传入的是Activity的Context,在关闭Activity时,Activity的内存无法被回收,因为单例持有Activity的引用。
  • 在Context的使用上,应该传入Application的Content到单例模式中,这样就保证了单例的生命周期跟Application的生命周期一样。
  • 因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明如果一个对象已经不需要使用了,而单例对象还在持有该对象的引用,那么这个对象就不能被正常回收,这就导致了内存泄漏。
  • 单例模式尽量少持有生命周期不同的外部对象,一旦持有必须在该对象的生命周期结束前null
 public class TestManager {
     private static TestManager instance;
     private Context context;
 
     private TestManager(Context context) {
         this.context = context;
     }
 
     public static TestManager getInstance(Context context) {
         if (instance != null) {
             instance = new TestManager(context);
         }
         return instance;
     }
 }

这是一个普通的单例模式,当创建这个单例的时候需要传入一个Context,所以这个Context的生命周期的长短至关重要:

  1. 如果传入的是Application的Context:没有任何问题,因为单例的生命周期和Application的一样长;
  2. 如果传入的是Activity的Context:当这个Context所对应的Activity退出时,因为该Context和Activity的生命周期一样长(Activity间接继承与Context),所以当前的Activity退出时内存并不会回收,因为单例对象持有该Activity的引用。
public class TestManager {
	    private static TestManager instance;
	    private Context context;
	
	    private TestManager(Context context) {
	        this.context = context.getApplicationContext();
	    }
	
	    public static TestManager getInstance(Context context) {
	        if (instance != null) {
	            instance = new TestManager(context);
	        }
	        return instance;
	    }
	}

正确的写法应该这样,不管传什么Context最终都使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

9. 不使用String进行拼接字符串
  • 严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄露。
  • 频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。
10. 三方库
  • 比如EventBus、RxJava等一些第三方开源框架的使用,如果在Activity销毁前没有进行解除订阅会导致内存泄漏。

你可能感兴趣的:(Android,内存泄露,内存溢出,内存抖动)