Android 内存泄漏总结

本文详细介绍了内存泄漏的定义、影响、内存泄漏与内存溢出的关系,探讨了JVM的内存回收机制,包括内存模型、对象可达性分析、堆内存回收策略和回收算法。同时,列举了常见的内存泄漏问题及其解决方法,以及AndroidStudio和Eclipse中的内存分析工具如MemoryProfiler、MAT和LeakCanary的使用方法。
摘要由CSDN通过智能技术生成

一 什么是内存泄漏

【定义】内存泄漏(Memory Leak),是指程序申请内存后,当该内存不再需要,但无法被释放的现象

【影响】容易造成应用发生内存溢出(OOM-Out Of Memory),致使应用闪退

【本质】生命周期较长的对象持有生命周期较短的对象的引用

【内存泄漏与内存溢出】

如示例图所示,堆空间正常驻存对象数为8,空闲对象数为4,泄漏对象数为4,若待分配对象数为6,正常驻存对象全部被强引用时,将无法分配,导致发生 OOM。

内存泄漏不会必然造成内存溢出,但是大对象或多对象的内存泄漏会显著增加内存溢出的风险。

二 JVM 内存回收机制

1 JVM 内存模型

Java 程序加载到虚拟机后,对应的数据会存储到 JVM 运行时数据区对应的存储区域。其中方法区,堆为线程共享;虚拟机栈,本地方法栈,程序计数器为线程私有区域。

堆区为线程共享,承担对象的分配和回收,是内存泄漏发生的主要区域

2 对象可达性分析

JVM 把对象中的引用看做一张图,从 GC Root 搜索引用链,根据引用链是否可达来决定是否回收对象,GC Root 对象本身是可达的不 GC 回收。

可用作 GC Root 的对象有:

  1. Java 虚拟机栈中引用对象,各个线程调用的参数、局部变量、临时变量等。
  2. 方法区中的类静态属性引用对象,比如引用类型的静态变量。
  3. 方法区中的常量引用对象。
  4. 本地方法栈的 JNI(native 方法)引用对象。
  5. Java 虚拟机内部的引用,基本数据类型对应的 Class 对象,一些常驻的异常对象。
  6. 被同步锁(synchronized)持有的对象。

3 堆内存回收

3.1 回收策略

Java堆区可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 0区、Survivor 1区。具体比例参数如下图所示

JVM 使用分代回收策略进行堆内存的管理和回收,整体流程如下:

1 对象默认在新生代 Eden 区分配,当 Eden 区满后,执行 GC,将存活的对象复制到 S0 区。

2 当下一次 Eden 区满时,执行对 Eden 和 S0 区 GC,将存活对象复制到 S1 区域。

3 Eden 满触发一定次数后,将依旧存活的对象复制到老年代中。

发生在新生代的 GC 叫做 minor GC,发生在老年代的 GC 叫做 major GC,若同时在新生代和老年代发生 GC 则成为 Full GC。

3.2 回收算法

3.2.1 标记-清除(Tracing Collector)


标记-清除(Tracing Collector)算法是最基础的收集算法。它使用了根集的概念,分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象。

优点: 不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。

缺点: 标记和清除过程的效率都不高,需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片。

3.2.2 标记整理算法 (Compacting Collector)

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

优点: 经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片问题。

缺点: GC 耗时更多,因为需要将所有对象都拷贝到新地方,且需更新它们的引用地址。

3.2.3 复制(Copying Collector)

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

优点: 标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。

缺点: 需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。

三 常见内存泄漏问题

1 Static 引用

【原因】若对象被 static 变量持有,引用耗费资源过多的实例(如 Context),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露。

【代码示例】

public class LeakSingleton {
  private static LeakSingleton sInstance;
  private Context mContext;

    private LeakSingleton(Context context) {
      mContext = context;
    }
    // LeakSingleton 传入 Context 对象引用,若 context 为 Activity,则 Activity 生命周期结束后,将继续被 LeakSignleton 持有
    public synchronized LeakSingleton getInstance(Context context) {
      return new LeakSingleton(context);
    }
}

【典型场景】单例中使用 context 作为单例对象的成员变量,context 传入 Activity

2 非静态内部类

【原因】非静态内部类会持有外部类对象的引用,若非静态内部类对象生命周期超过外部类对象,则会发生外部类对象的内存泄漏。

【代码示例】

public class MainActivity extends AppCompatActivity {
  // mHandler 引用匿名 Handler 对象
  private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
    // 业务逻辑
    }
  };

  private void sendDelayMessage() {
  // 发送延时消息 10s 后执行,若 Activity 在此之前销毁,将被 mHandler 持有,发生泄漏
    mHandler.sendEmptyMessageDelayed(1, 10 * 1000);
  }

  private void doWork() {
    // 构建匿名 Thread 类对象执行耗时操作
    new Thread(new Runnable() {
      @Override
      public void run() {
      // 执行耗时操作,若在耗时操作完成前,Activity 销毁,将被 Thread 对象持有,发生泄漏
      }
    }).start();
  }
}

【典型场景】Handler 发送延迟消息,开辟工作线程执行耗时操作。

3 资源对象使用后未关闭

【原因】 资源对象使用后未关闭,在 Activity/Fragemnt 销毁时没有关闭/注销这些资源,将导致无法回收。

【代码示例】

// 动态广播接收器未反注册
unregisterReceiver(mReceiver);
// 文件流操作未调用 close 关闭
InputStream inputStream = new FileInputStream(new File(inputFilePath));
OutputStream outputStream = new FileOutputStream(new File(outFilePath));
inputStream.close();
outputStream.close();
// 数据库游标未调用 close 方法
Cursor cursor = getContentResolver().query(url, projection, selection, null);
cursor.close();
// 无限重复执行动画
ObjectAnimator animator = ObjectAnimator.ofInt(view, property, startValue, endValue);
animator.setRepeatMode(ValueAnimator.INFINITE);
animator.start();
// 未调用 cancel
animator.cancel();

【典型场景】广播 BraodcastReceiver、文件流操作、数据库游标 Cursor、循环动画

四 常用内存泄漏分析工具

1 MemoryProfiler

MemoryProfiler 是 AndroidStudio 自带的内存分析工具,适用于在本地进行应用内存问题分析,其界面如下:

我们可以使用图中 2 所示功能执行 hprof dump,获取内存快照,根据 MemoryProfiler 提供的 leak 分析,确认程序中是否存在 Activity 或者 Fragment 泄漏。

​详细的介绍和使用方法,请参考: https://developer.android.com/studio/profile/memory-profiler?hl=zh-cn

2 MAT(Memory Analyzer)

MAT 是 Eclipse 开发的强大堆内存分析工具,是分析内存使用和内存泄漏的利器。实际工作中,常用 Path to GC Roots 功能,分析对象的强引用链,从而定位内存泄漏问题。

上图展示了查看 Account 类强引用链的方法。

详细介绍和使用方法,请参考: http://wiki.eclipse.org/MemoryAnalyzer

3 LeakCanary

LeakCanary 是由 Square 公司开发的内存泄漏检测库,通过集成到 APK 中,方便应用开发者在线下开发调试阶段,发现内存泄漏问题,提供直观的内存泄漏原因 UI 展现。

LeakCanary 会将内存泄漏进行归类展示,如上图左侧图所示,检测出了 4 类泄漏(4 Distinck Leaks)。

如上图右侧所示,每个类别会呈现具体的泄漏引用链,在引用链的底部,显示了具体的泄漏原因。(TextView 成员变量 mContext 引用了销毁的 activity)。

详细介绍和使用方法,请参考: https://square.github.io/leakcanary/

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值