科普:内存泄漏与内存溢出

作者: 大利猫 | 来源:发表于2016-04-24 18:44 被阅读2429次

    最近项目中频繁出现OOM的问题,各种路径测试、内存走向分析、各种逻辑推理才最终定位到问题。在这过程中和组内的同学讨论的时候发现有的同学对内存泄漏和内存溢出的概念理解不到位,导致沟通过程比较尴尬。很多同学对这两个概念理解不够透彻,在项目中频繁写出内存泄漏的低级代码出来。结合自己的理解我写一篇文章理解下这两个概念。

    内存泄漏

    内存泄漏是指那些本应该回收(不再使用)的内存对象无法被系统回收的现象。在c++中需要程序猿手动释放内存对象,所以在C++中更容易存在内存泄漏。java引入了自动回收机制,使得在C++中令人头疼的内存问题得到了有效的改善,但这并不意味着java程序员不关注内存,因为垃圾回收机制不能完全保证内存对象在该释放的地方释放,现代java虚拟机中普遍使用根集算法去计算对象的引用可达性,不可达的才能回收,例如下图中的无用对象被有用对象引用着,导致无用对象引用一直可达,系统回收器不敢冒然回收,从而造成内存泄漏。

    内存泄漏.png

    内存溢出

    系统在为某段执行指令(程序)分配内存的时候,发现内存不足,抛出错误,这叫做内存溢出。

    内存溢出

    二者关系

    手机设备的内存空间是有限的,为每个应用所分配到的内存空间更是有限的,当内存泄漏对象越来越多,可调配内存空间就越小,App应用性能越差,当可调配的内存空间不够创建新对象时就会引起OOM。

    内存泄漏与溢出.png

    内存泄漏经典模型

    静态变量

    静态变量的生命周期是最长的,和应用程序的生命周期一样,当一个大对象被一个类的静态变量引用时,这个对象就无法被系统回收,在应用的整个生命周期中占用内存。常见于工具类,一般中存在大量的工具类,很多同学图方便直接或间接使用静态变量引用一个上下文对象的。

     public class NotificationUtil { 
     //静态变量,notificationManager泄漏
      private static  NotificationManager notificationManager;   
      public static void notification(Context context, Class<?> cls, Message msg) {   
         if (notificationManager == null){    
            notificationManager = (NotificationManager)       
            context.getSystemService(context.NOTIFICATION_SERVICE);   
          }
        //....
     }
    }
    

    NotificationManager 对象泄漏了,同时因为 NotificationManager 对象中有一个上下文对象mContext变量,又回引起启动这个方法的Context对象泄漏。

    规避:

    对于工具类,如非频繁使用的对象,尽量不要使用 static 变量去引用,可以在方法执行时候再创建,作为局部变量使用;如需要频繁使用,为了提高方法执行效率,对于上面这种情况可以把Context 参数限制为Application 级别的上下文,避免调用方传递Activity级别的上下文,造成Activity泄漏。

    public class NotificationUtil { 
     //静态变量
      private static  NotificationManager notificationManager;   
      public static void notification(Application context, Class<?> cls, Message msg) {   
        //context限制为Application级别
         if (notificationManager == null){    
            notificationManager = (NotificationManager)       
            context.getSystemService(context.NOTIFICATION_SERVICE);   
          }
        //.....
     }
    }
    

    单例模式

    单例模式其实也是静态变量的一种,单例的生命周期和静态变量时一样的,如果这个单例中持有一个大对象,就会引起这个大对象泄漏。

    private static WebViewClient instance;
    public static WebViewClient getInstance(Context context) {   
     if (instance == null) {  
        //mContext有泄漏风险
        instance = new WebViewClient(context, jsToJava); 
      }   
     return instance;
    } 
    

    例如这里的mContext,如果是个Activity的话,会被instance长期引用着的。

    规避:

    和静态变量一样的道理,尽量使用Application级别的上下文代替Activity级别的上下文。

    private static WebViewClient instance;
    public static WebViewClient getInstance(Application context) {   
      //context限制为Application级别的
     if (instance == null) {  
        instance = new WebViewClient(context, jsToJava); 
      }   
     return instance;
    }
    

    内部类

    由于内部类的存在需要依赖它的外部类,由于某些原因导致内部类被引用会无法退出,引起外部类无法回收,这是使用最多也是最容易被用出内存泄漏的了。

    内部类循环.png

    内部类循环或者阻塞,下面就有一个奇葩的代码,一个研究生写的:

    import android.content.Context;
    import android.util.AttributeSet;
    import android.widget.EditText;
    public class CheckEditText extends EditText{
     public CheckEditText(Context context,AttributeSet attrs){
      super(context,attrs);
      new Thread(){
       @Override
       public void run(){
          while(true) {
             String content = this.getText().toString();
             if(content.length() > 12){
                //字符长度不能大于12
                //...
             }
         }
       }
      }.start();
     }
    }
    

    然后所有使用这个CheckEditText的Activity都泄漏了。

    内部类被其他对象持有.png

    这种模式最常见的就是Handler的使用了,一般项目中很多内存泄漏就是这种模型:

    //内部类的Handler,有内存泄漏风险
    Handler handler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            int what = msg.what;
            switch (what) {
                case SHOW:
                    progressDialog = ProgressDialog.show(DetailActivity.this, null, "图片上传中...");
                    break;
                case DISMISS:
                    if (progressDialog != null && progressDialog.isShowing()) {
                        progressDialog.dismiss();
                    }
                    break;
                case UPLOADPIC:
                    String base64ImageString = (String) msg.obj;
                    webView.loadUrl("javascript:fileChooserCallback(" + "'" + base64ImageString
                            + "'" + ")");
                    break;
    
                default:
                    break;
            }
    
        }
    
    };
    

    这是DetailActivity中的一个Handler内部类,这会导致DetailActivity泄漏,因为Handler其实是被一个消息队列引用着。

    规避:

    对于那些可能长时间执行、阻塞或者被外部引用的内部尽量使用静态内部类代替。静态内部类对象的存在不依附外部类,这样可以避开内部类对外部类的隐性引用,然后使用弱引用持有外部类对象。

     static class MyHandler extends  Handler {
          //静态内部类代替,并使用若引用持有DetailActivity
        private WeakReference<DetailActivity> weakReference;
        public MyHandler(DetailActivity activity) {
            this.weakReference = new WeakReference(activity);
        }
        public void handleMessage(android.os.Message msg) {
            int what = msg.what;
            DetailActivity activity = weakReference.get();
            if (activity == null){
                return;
            }
            switch (what) {
                case SHOW:
                    activity.progressDialog = ProgressDialog.show(activity, null, "图片上传中...");
                    break;
                case DISMISS:
                    if (activity.progressDialog != null && activity.progressDialog.isShowing()) {
                        activity.progressDialog.dismiss();
                    }
                    break;
                case UPLOADPIC:
                    String base64ImageString = (String) msg.obj;
                    activity.webView.loadUrl("javascript:fileChooserCallback(" + "'" + base64ImageString
                            + "'" + ")");
                    break;
                default:
                    break;
            }
    
        }
    };
    MyHandler handler = new MyHandler(this);

    相关文章

      网友评论

      • 4beaa5f8876a:Good job
      • liut_2016:受教了
      • 飞虎兄:写的很到位,总结的也很好。毕设android程序经常内存泄露,对小白的我来说排查很麻烦。
      • d07e1834a502:简书上内容都是比较新而且比较精,博主写的很好.
      • 217cce305b3c:楼主,推荐下在Android开发中有哪些检测工具?
        大利猫:@奇幻森林 memory monitor,leakcanary ,mat 足矣
      • HuDP:理解又深入了些 :+1:
      • c40713386765:Android可以用Square的LeakCanary去辅助检测内存泄漏,蛮好用的
        推荐给组员看看《深入理解Java虚拟机》呗,虽然我买了好久都没看完.... :joy:
        大利猫:@嗣音君 good
      • 4f76b4cf8633:大哥你可以再写些类似的问题 有很多人(不是科班出身的 或 学了忘了)不明白或者模糊笼统的一些概念 :smiley:
      • 2cab9682f267:无用代码占用内存---内存泄露;有用代码分配不到内存---内存溢出!叫泄漏是因为内存分配给了无效代码,叫溢出是因为有用代码之和大于内存
        大利猫:@歌风丶唯幻云 推荐《深入理解Java虚拟机》
        2cab9682f267: @大利猫 嗯嗯!请问有哪些书籍讲解Java或者Android程序运行时的内存分配以及内部对象的管理问题呢?望推荐
        大利猫:@歌风丶唯幻云 无用代码?无用对象更专业些
      • Aaaabccz:理论加实践,还有自己的见解,赞。
      • cuixbo:写的很好,将内存溢出和内存泄露讲的很详细明了。赞👍
      • 3f98f64e96fc:写的不错

      本文标题:科普:内存泄漏与内存溢出

      本文链接:https://www.haomeiwen.com/subject/tyyfrttx.html