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销毁前没有进行解除订阅会导致内存泄漏。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值