看完这篇文章,处理 APP 中 90 % 的内存异常问题

作者 : 开心源码 本文共12125个字,预计阅读时间需要31分钟 发布时间: 2022-05-13 共202人阅读

我们为什么要优化内存

在 Android 中我们写的 .java 文件,最终会编译成 .class 文件, class 又由类装载器加载后,在 JVM 中会形成一份形容 class 结构的元信息对象,通过该元信息对象可以知道 class 的结构信息 (构造函数、属性、方法)等。

JVM 会把形容类的数据从 class 文件加载到内存,Java 有一个很好的管理内存的机制,垃圾回收机制 GC 。

为什么 Java 都给我们提供了垃圾回收机制,程序有时还会导致内存泄漏,内存溢出 OOM,甚至导致程序 Crash 。接下来我们就对实际开发中出现的这些内存问题,来进行优化。

JAVA 虚拟机

我们先来大概理解一下 Java 虚拟机里面运行时的数据区域有哪些。

线程独占区

程序计数器

  • 相当于一个执行代码的指示器,用来确认下一行执行的地址
  • 每个线程都有一个
  • 没有 OOM 的区

虚拟机栈

  • 我们平常说的栈就是这块区域
  • java 虚拟机规范中定义了 OutOfMemeory , stackoverflow 异常

本地方法栈

  • java 虚拟机规范中定义了 OutOfMemory ,stackoverflow 异常

注意

  • 在 hotspotVM 中把虚拟机栈和本地方法栈合为了一个栈区

线程共享区

方法区

  • ClassLoader 加载类信息
  • 常量、静态变量
  • 编译后的代码
  • 会出现 OOM
  • 运行时常量池
    • public static final
    • 符号引用类、接口全名、方法名

java 堆 (本次需要优化的地方)

  • 虚拟机能管理的最大的一块内存 GC 主战场
  • 会出现 OOM
  • 对象实例
  • 数据的内容

JAVA GC 如何确定内存回收

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,假如不及时进行回收,会降低程序运行效率,甚至引发系统异常。

目前虚拟机基本都是采用可达性分析算法,为什么不采用引用计数算法呢?下面就说说引用计数法是假如统计所有对象的引用计数的,再比照可达性分析算法是如何处理引用计数算法的不足。下面就来看下这 2 个算法:

引用计数算法

每个对象有一个引用计数器,当对象被引用一次则计数器加一,当对象引用一次失效一次则计数器减一,对于计数器为 0 的时候就意味着是垃圾了,可以被 GC 回收。

下面通过一段代码来实际看下

public class GCTest {    private Object instace = null;    public static void onGCtest() {        //step 1        GCTest gcTest1 = new GCTest();        //step 2        GCTest gcTest2 = new GCTest();        //step 3        gcTest1.instace = gcTest2;        //step 4        gcTest2.instace = gcTest1;        //step 5        gcTest1 = null;        //step 6        gcTest2 = null;    }    public static void main(String[] arg) {        onGCtest();    }}

分析代码

//step 1 gcTest1 引用 + 1 = 1//step 2 gcTest2 引用 + 1 = 1//step 3 gcTest1 引用 + 1 = 2//step 4 gcTest2 引用 + 1 = 2//step 5 gcTest1 引用 - 1 = 1//step 6 gcTest2 引用 - 1 = 1

很显著现在 2 个对象都不能用了都为 null 了,但是 GC 确不能回收它们,由于它们本身的引用计数不为 0 。不能满足被回收的条件,虽然调用 System.gc() 也还是不能得到回收, 这就造成了 内存泄漏 。当然,现在虚拟机基本上都不采用此方式。

可达性分析算法

从 GC Roots 作为起点开始搜索,那么整个连通图中额对象边都是活对象,对于 GC Roots 无法到达的对象便成了垃圾回收的对象,随时可能被 GC 回收。

可以作为 GC Roots 的对象

  • 虚拟机栈正在运行使用的引用
  • 静态属性 常量
  • JNI 引用的对象

GC 是需要 2 次扫描才回收对象,所以我们可以使用 finalize 去救活丢失的引用

 @Override    protected void finalize() throws Throwable {        super.finalize();        instace = this;    }

到了这里,相信大家已经能够弄明白这 2 个算法的区别了吧?反正对于对象之间循环引用的情况,引用计数算法无法回收这 2 个对象,而可达性是从 GC Roots 开始搜索,所以能够正确的回收。

不同引用类型的回收状态

强引用

Object strongReference = new Object()

假如一个对象具备强引用,那垃圾回收器绝不会回收它,当内存空间不足, Java 虚拟机宁愿抛出 OOM 错误,使程序异常 Crash ,也不会靠随便回收具备强引用的对象来处理内存不足的问题.假如强引用对象不再使用时,需要弱化从而使 GC 能够回收,需要:

strongReference = null; //等 GC 来回收

还有一种情况,假如:

public void onStrongReference(){    Object strongReference = new Object()}

在 onStrongReference() 内部有一个强引用,这个引用保存在 java 栈 中,而真正的引用内容 (Object)保存在 java 堆中。当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为 0 ,这个对象会被回收。

但是假如 mStrongReference 引用是全局时,就需要在不用这个对象时赋值为 null ,由于 强引用 不会被 GC 回收。

软引用 (SoftReference)

假如一个对象只具备软引用,则内存空间足够,垃圾回收器就不会回收它;假如内存空间不足了,就会回收这些对象的内存,只需垃圾回收器没有回收它,该对象即可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,假如软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。

注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。

弱引用 (WeakReference)

弱引用与软引用的区别在于: 只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具备弱引用的对象,不论当前内存空间足够与否,都会回收它的内存。不过因为垃圾回收器是一个优先级很低的线程,因而不肯定会很快发现那些只具备弱引用的对象。

弱引用可以和一个引用队列联合使用,假如弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。

注意这里的 referenceQueuee 是装的被回收的对象。

虚引用 (PhantomReference)

    @Test    public void onPhantomReference()throws InterruptedException{        String str = new String("123456");        ReferenceQueue queue = new ReferenceQueue();        // 创立虚引用,要求必需与一个引用队列关联        PhantomReference pr = new PhantomReference(str, queue);        System.out.println("PhantomReference:" + pr.get());        System.out.printf("ReferenceQueue:" + queue.poll());    }

虚引用顾名思义,就是形同虚设,与其余几种引用都不同,虚引用并不会决定对象的生命周期。假如一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必需和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,假如发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

总结

引用类型调用方式GC能否内存泄漏
强引用直接调用不回收
软引用.get()视内存情况回收
弱引用.get()回收不可能
虚引用null任何时候都可能被回收,相当于没有引用一样

分析内存常用工具

工具很多,掌握原理方法,工具随便筛选使用。

  • top/procrank
  • meinfo
  • Procstats
  • DDMS
  • MAT
  • Finder – Activity
  • LeakCanary
  • LeakInspector

内存泄漏

产生的起因: 一个长生命周期的对象持有一个短生命周期对象的引用,浅显点讲就是该回收的对象,由于引用问题没有被回收,最终会产生 OOM。

下面我们来利用 Profile 来检查项目能否有内存泄漏

怎样利用 profile 来查看项目中能否有内存泄漏

1. 在 AS 中项目以 profile 运行

2. 在 MEMORY 界面中选择要分析的一段内存,右键 export

Allocations: 动态分配对象个数
Deallocation: 解除分配的对象个数
Total count: 对象的总数
Shalow Size: 对象本身占用的内存大小
Retained Size: GC 回收能收走的内存大小

3. 转换 profile 文件格式

*   将 export 导出的 dprof 文件转换为 Mat 的 dprof 文件*   cd /d 进入到 Android sdk/platform-tools/hprof-conv.exe    ```    //转换命令 hprof-conv -z src des    D:\Android\AndroidDeveloper-sdk\android-sdk-windows\platform-tools>hprof-conv -z D:\temp_\temp_6.hprof D:\temp_\memory6.hprod    ```

4. 下载 Mat 工具

5. 打开 MemoryAnalyzer.exe 点击左上角 File 菜单中的 Open Heap Dupm

6. 查看内存泄漏中的 GC Roots 强引用

这里我们得知是一个 ilsLoginListener 引用了 LoginView,我们来看下代码最后怎样处理的。

代码中我们找到了 LoginView 这个类,发现是一个单例中的回调引起的内存泄漏,下面怎样处理勒,请看第七小点。

7. 2种处理单例中的内存泄漏

  1. 将引用置为 null
        /**             * 销毁监听             */            public void unRemoveRegisterListener(){                mMessageController.unBindListener();            }            public void unBindListener(){                if (listener != null){                    listener = null;                }            }
  1. 使用弱引用

    ```//将监听器放入弱引用中WeakReference<IBinderServiceListener> listenerWeakReference = new WeakReference<>(listener);//从弱引用中取出回调listenerWeakReference.get();```
8. 通过第七小点就能完美的处理单例中回调引起的内存泄漏。

Android 中常见的内存泄漏经典案例及处理方法

单例

**示例 :**```public class AppManager {    private static AppManager sInstance;    private CallBack mCallBack;    private Context mContext;    private AppManager(Context context) {        this.mContext = context;    }    public static AppManager getInstance(Context context) {        if (sInstance == null) {            sInstance = new AppManager(context);        }        return sInstance;    }    public void addCallBack(CallBack call){        mCallBack = call;    }}```
  1. 通过上面的单列,假如 context 传入的是 Activity , Service 的 this,那么就会导致内存泄漏。

以 Activity 为例,当 Activity 调用 getInstance 传入 this ,那么 sInstance 就会持有 Activity 的引用,当 Activity 需要关闭的时候需要 回收的时候,发现 sInstance 还持有 没有用的 Activity 引用,导致 Activity 无法被 GC 回收,就会造成内存泄漏

  1. addCallBack(CallBack call) 这样写看起来是没有毛病的。但是当这样调用在看一下勒。

    ```//在 Activity 中实现单例的回调AppManager.getInstance(getAppcationContext()).addCallBack(new CallBack(){    @Override    public void onStart(){    }});```

这里的 new CallBack() 匿名内部类 默认持有外部的引用,造成 CallBack 释放不了,那么怎样处理了,请看下面处理方法

处理方法:

  1. getInstance(Context context) context 都传入 Appcation 级别的 Context,或者者实在是需要传入 Activity 的引用就用 WeakReference 这种形式。

  2. 匿名内部类建议大家单独写一个文件或者者

    ```public void addCallBack(CallBack call){        WeakReference<CallBack> mCallBack= new WeakReference<CallBack>(call);    }```
  3. Handler

    示例:

    //在 Activity 中实现 Handlerclass MyHandler extends Handler{    private Activity m;    public MyHandler(Activity activity){        m=activity;    }//    class.....}

这里的 MyHandler 持有 activity 的引用,当 Activity 销毁的时候,导致 GC 不会回收造成 内存泄漏。

处理方法:

```1.使用静态内部类 + 弱引用2.在 Activity onDestoty() 中解决  removeCallbacksAndMessages()     @Override    protected void onDestroy() {        super.onDestroy();    if(null != handler){          handler.removeCallbacksAndMessages(null);          handler = null;    } }```
  1. 静态变量

    示例:

    public class MainActivity extends AppCompatActivity {    private static Police sPolice;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        if (sPolice != null) {            sPolice = new Police(this);        }    }}class Police {    public Police(Activity activity) {    }}

这里 Police 持有 activity 的引用,会造成 activity 得不到释放,导致内存泄漏。

处理方法:

```//1\. sPolice 在 onDestory()中 sPolice = null;//2\. 在 Police 构造函数中 将强引用 to 弱引用;```
  1. 非静态内部类

参考 第二点 Handler 的解决方式

  1. 匿名内部类

示例:

public class MainActivity extends AppCompatActivity {        @Override        protected void onCreate(Bundle savedInstanceState) {            super.onCreate(savedInstanceState);            setContentView(R.layout.activity_main);            new Thread(){                 @Override                 public void run() {                    super.run();                            }                        };        }    }

很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程ThreadAsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。

处理方法:

//静态内部类 + 弱引用//单独写一个文件 + onDestory  = null;
  1. 未取消注册或者回调

示例:

public class MainActivity extends AppCompatActivity {        @Override        protected void onCreate(Bundle savedInstanceState) {            super.onCreate(savedInstanceState);            setContentView(R.layout.activity_main);            registerReceiver(mReceiver, new IntentFilter());        }        private BroadcastReceiver mReceiver = new BroadcastReceiver() {            @Override            public void onReceive(Context context, Intent intent) {                // TODO ------            }        };    }

在注册观察则模式的时候,假如不及时取消也会造成内存泄露。比方使用Retrofit + RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者者销毁的时候取消注册。

处理方法:

//Activity 中实现 onDestory()反注册广播得到释放        @Override        protected void onDestroy() {            super.onDestroy();            this.unregisterReceiver(mReceiver);        }
  1. 定时任务

示例:

public class MainActivity extends AppCompatActivity {        /**模拟计数*/        private int mCount = 1;        private Timer mTimer;        private TimerTask mTimerTask;        @Override        protected void onCreate(Bundle savedInstanceState) {            super.onCreate(savedInstanceState);            setContentView(R.layout.activity_main);            init();            mTimer.schedule(mTimerTask, 1000, 1000);        }        private void init() {            mTimer = new Timer();            mTimerTask = new TimerTask() {                @Override                public void run() {                    MainActivity.this.runOnUiThread(new Runnable() {                        @Override                        public void run() {                            addCount();                        }                    });                }            };        }        private void addCount() {          mCount += 1;        }    }

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity 的引用不能被 GC 回收,因而当我们 Activity 销毁的时候要立即cancelTimerTimerTask,以避免发生内存泄漏。

处理方法:

//当 Activity 关闭的时候,中止一切正在进行中的定时任务,避免造成内存泄漏。       private void stopTimer() {           if (mTimer != null) {               mTimer.cancel();               mTimer = null;           }           if (mTimerTask != null) {               mTimerTask.cancel();               mTimerTask = null;           }       }       @Override       protected void onDestroy() {           super.onDestroy();           stopTimer();       }
  1. 资源未关闭

示例:

ArrayList,HashMap,IO,File,SqLite,Cursor 等资源用完肯定要记得 clear remove 等关闭一系列对资源的操作。

处理方法:

    用完即刻销毁
  1. 属性动画

    示例:

动画同样是一个耗时任务,比方在 Activity 中启动了属性动画 (ObjectAnimator) ,但是在销毁的时候,没有调用 cancle 方法,尽管我们看不到动画了,但是这个动画仍然会不断地播放下去,动画引用所在的控件,所在的控件引用 Activity ,这就造成 Activity 无法正常释放。因而同样要在Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。

处理方法:

    @Override    protected void onDestroy() {        super.onDestroy();        //当关闭 Activity 的时候记得关闭动画的操作        mAnimator.cancel();    }
  1. Android 源码或者者第三方 SDK

示例:

假如在开发调试中遇见 Android 源码或者者 第三方 SDK 持有了我们当前的 Activity 或者者其它类,那么现在怎样办了。

处理方法:

当前是通过 Java 中的反射找到某个类或者者成员,来进行手动 = null 的操作。

内存抖动

什么是内存抖动

内存频繁的分配与回收,(分配速度大于回收速度时) 最终产生 OOM 。

也许下面的录屏更能解释什么是内存抖动

可以看出当我点击了一下 Button 内存就频繁的创立并回收(注意看垃圾桶)。

那么我们找出代码中具体那一块出现问题了勒,请看下面一段录屏

mButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                imPrettySureSortingIsFree();            }        });/**     *&emsp;排序后打印二维数组,一行行打印     */    public void imPrettySureSortingIsFree() {        int dimension = 300;        int[][] lotsOfInts = new int[dimension][dimension];        Random randomGenerator = new Random();        for (int i = 0; i < lotsOfInts.length; i++) {            for (int j = 0; j < lotsOfInts[i].length; j++) {                lotsOfInts[i][j] = randomGenerator.nextInt();            }        }        for (int i = 0; i < lotsOfInts.length; i++) {            String rowAsStr = "";            //排序            int[] sorted = getSorted(lotsOfInts[i]);            //拼接打印            for (int j = 0; j < lotsOfInts[i].length; j++) {                rowAsStr += sorted[j];                if (j < (lotsOfInts[i].length - 1)) {                    rowAsStr += ", ";                }            }            Log.i("ricky", "Row " + i + ": " + rowAsStr);        }    }

最后我们之后是 onClick 中的 imPrettySureSortingIsFree() 函数里面的 rowAsStr += sorted[j]; 字符串拼接造成的 内存抖动 ,由于每次拼接一个 String 都会申请一块新的堆内存,那么怎样处理这个频繁开拓内存的问题了。

其实在 Java 中有 2 个更好的 API 对 String 的操作很友好,相信应该有人猜到了吧。没错就是将 此处的 String 换成 StringBuffer 或者者 StringBuilder,就能很完美的处理字符串拼接造成的内存抖动问题。

修改后

        /**         *&emsp;打印二维数组,一行行打印         */        public void imPrettySureSortingIsFree() {            int dimension = 300;            int[][] lotsOfInts = new int[dimension][dimension];            Random randomGenerator = new Random();            for(int i = 0; i < lotsOfInts.length; i++) {                for (int j = 0; j < lotsOfInts[i].length; j++) {                    lotsOfInts[i][j] = randomGenerator.nextInt();                }            }            // 使用StringBuilder完成输出,我们只要要创立一个字符串就可,              不需要白费过多的内存            StringBuilder sb = new StringBuilder();            String rowAsStr = "";            for(int i = 0; i < lotsOfInts.length; i++) {                // 清理上一行                sb.delete(0, rowAsStr.length());                //排序                int[] sorted = getSorted(lotsOfInts[i]);                //拼接打印                for (int j = 0; j < lotsOfInts[i].length; j++) {                    sb.append(sorted[j]);                    if(j < (lotsOfInts[i].length - 1)){                        sb.append(", ");                    }                }                rowAsStr = sb.toString();                Log.i("jason", "Row " + i + ": " + rowAsStr);            }        }

总结

只需养成这样的习惯,至少可以避免 90 % 以上不会造成内存异常

**1. 数据类型: **不要使用比需求更占用空间的基本数据类型
2. 循环尽量用 foreach ,少用 iterator, 自动装箱也尽量少用
3. 数据结构与算法的解度解决 (数组,链表,栈树,树,图)

数据量千级以内可以使用 Sparse 数组 (Key为整数),ArrayMap (Key 为对象) 尽管性能不如 HashMap ,但节约内存。

4. 枚举优化

缺点:

每一个枚举值都是一个单例对象,在使用它时会添加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存

较多的使用 Enum 会添加 DEX 文件的大小,会造成运行时更多的 IO 开销,使我们的应用需要更多的空间

特别是分 Dex 多的大型 APP,枚举的初始化很容易导致 ANR

优化后的代码:可以直接限定传入的参数个数

    public class SHAPE {        public static final int TYPE_0=0;        public static final int TYPE_1=1;        public static final int TYPE_2=2;        public static final int TYPE_3=3;        @IntDef(flag=true,value={TYPE_0,TYPE_1,TYPE_2,TYPE_3})        @Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})        @Retention(RetentionPolicy.SOURCE)        public @interface Model{        }        private @Model int value=TYPE_0;        public void setShape(@Model int value){            this.value=value;        }        @Model        public int getShape(){            return this.value;        }    }

5. static , static final 的问题

  • static 会由编译器调用 clinit 方法进行初始化
  • static final 不需要进行初始化工作,打包在 dex 文件中可以直接调用,并不会在类初始化申请内存

基本数据类型的成员,可以全写成 static final

6. 字符串的拼接尽量少用 +=

7. 重复申请内存问题

  • 同一个方法屡次调用,如递归函数 ,回调函数中 new 对象
  • 不要在 onMeause() onLayout() ,onDraw() 中去刷新UI(requestLayout)

8. 避免 GC 回收将来要重新使用的对象 (内存设计模式对象池 + LRU 算法)

9. Activity 组件泄漏

  • 非业务需要不要把 activity 的上下文做参数传递,可以传递 application 的上下文
  • 非静态内部类和匿名内部内会持有 activity 引用(静态内部类 或者者 单独写文件)
  • 单例模式中回调持有 activity 引用(弱引用)
  • handler.postDelayed() 问题
  • 假如开启的线程需要传入参数,用弱引接收可处理问题
  • handler 记得清理 removeCallbacksAndMessages(null)

10. Service 耗时操作尽量使用 IntentService,而不是 Service

最后

假如你觉得文章写得不错就给个赞呗?假如你觉得那里值得改进的,请给我留言。肯定会认真查询,修正不足。谢谢。

希望读到这的您能转发分享和关注一下我,以后还会升级技术干货,谢谢您的支持!

转发+点赞+关注,第一时间获取最新知识点

Android架构师之路很漫长,一起共勉吧!


以下墙裂推荐阅读!!!

  • Android学习笔记参考(敲黑板!!)
  • “寒冬未过”,阿里P9架构分享Android必备技术点,让你offer拿到手软!
  • 毕业3年,我是如何从年薪10W的拖拽工程师成为30W资深Android开发者!
  • 腾讯T3大牛带你理解 2019 Android开发趋势及必备技术点!
  • 八年Android开发,从码农到架构师分享我的技术成长之路,共勉!

最后祝大家生活愉快~

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 看完这篇文章,处理 APP 中 90 % 的内存异常问题

发表回复