系统剖析Android中的内存泄漏

作为Android开发人员,我们或多或少都听说过内存泄漏。那么何为内存泄漏,Android中的内存泄漏又是什么样子的呢,本文将简单概括的进行一些总结。

关于内存泄露的定义,我可以理解成这样

没有用的对象无法回收的现象就是内存泄露

如果程序发生了内存泄露,则会带来如下的问题

  • 应用可用的内存减少,增加了堆内存的压力
  • 降低了应用的性能,比如会触犯更频繁的GC
  • 严重的时候可能会导致内存溢出错误,即OOM Error

在正式介绍内存泄露之前,我们有必要介绍一些必要的预备知识。

预备知识1: Java中的对象

  • 当我们使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象
  • 创建的对象可以被局部变量,实例变量和类变量引用。
  • 通常情况下,类变量持有的对象生命周期最长,实例变量次之,局部变量最短。
  • 垃圾回收器回收非存活的对象,并释放对应的内存空间。

预备知识2:Java中的GC

  • 和C++不同,对象的释放不需要手动完成,而是由垃圾回收器自动完成。
  • 垃圾回收器运行在JVM中
  • 通常GC有两种算法:引用计数和GC根节点遍历

引用计数

  • 每个对象有对应的引用计数器
  • 当一个对象被引用(被复制给变量,传入方法中),引用计数器加1
  • 当一个对象不被引用(离开变量作用域),引用计数器就会减1
  • 基于这种算法的垃圾回收器效率较高
  • 循环引用的问题引用计数算法的垃圾回收器无法解决。
  • 主流的JVM很少使用基于这种算法的垃圾回收器实现。

GC根节点遍历

  • 识别对象为垃圾从被称为GC 根节点出发
  • 每一个被遍历的强引用可到达对象,都会被标记为存活
  • 在遍历结束后,没有被标记为存活的对象都被视为垃圾,需要后续进行回收处理
  • 主流的JVM一般都采用这种算法的垃圾回收器实现

以上图为例,我们可以知道

  • 最下层的两个节点为GC Roots,即GC Tracing的起点
  • 中间的一层的对象,可以强引用到达GC根节点,所以被标记为存活
  • 最上层的三个对象,无法强引用达到GC根节点,所以无法标记为存活,也就是所谓的垃圾,需要被后续回收掉。

上面的垃圾回收中,我们提到的两个概念,一个是GC根节点,另一个是强引用

在Java中,可以作为GC 根节点的有

  • 类,由系统类加载器加载的类。这些类从不会被卸载,它们可以通过静态属性的方式持有对象的引用。注意,一般情况下由自定义的类加载器加载的类不能成为GC Roots
  • 线程,存活的线程
  • Java方法栈中的局部变量或者参数
  • JNI方法栈中的局部变量或者参数
  • JNI全局引用
  • 用做同步监控的对象
  • 被JVM持有的对象,这些对象由于特殊的目的不被GC回收。这些对象可能是系统的类加载器,一些重要的异常处理类,一些为处理异常预留的对象,以及一些正在执行类加载的自定义的类加载器。但是具体有哪些前面提到的对象依赖于具体的JVM实现。

提到强引用,有必要系统说一下Java中的引用类型。Java中的引用类型可以分为一下四种:

  • 强引用: 默认的引用类型,例如StringBuffer buffer = new StringBuffer();就是buffer变量持有的为StringBuilder的强引用类型。
  • 软引用:即SoftReference,其指向的对象只有在内存不足的时候进行回收。
  • 弱引用:即WeakReference,其指向的对象在GC执行时会被回收。
  • 虚引用:即PhantomReference,与ReferenceQueue结合,用作记录该引用指向的对象已被销毁。

补充了预备知识,我们就需要具体讲一讲Android中的内存泄漏了。

Android中的内存泄漏

归纳而言,Android中的内存泄漏有以下几个特点:

  • 相对而言,Android中的内存泄漏更加容易出现。
  • 由于Android系统为每个App分配的内存空间有限,在一个内存泄漏严重的App中,很容易导致OOM,即内存溢出错误。
  • 内存泄漏会随着App的推出而消失(即进程结束)。

在Android中的内存泄漏场景有很多,按照类型划分可以归纳为

  • 长期持有(Activity)Context导致的
  • 忘记注销监听器或者观察者
  • 由非静态内部类导致的

此外,如果按照泄漏的程度,可以分为

  • 长时间泄漏,即泄漏只能等待进程退出才消失
  • 短时间泄漏,被泄漏的对象后续会被回收掉。

长时间持有Activity实例

在Android中,Activity是我们常用的组件,通常情况下,一个Activity会包含了一些复杂的UI视图,而视图中如果含有ImageView,则有可能会使用比较大的Bitmap对象。因而一个Activity持有的内存会相对很多,如果造成了Activity的泄漏,势必造成一大块内存无法回收,发生泄漏。

这里举个简单的例子,说明Activity的内存泄漏,比如我们有一个叫做AppSettings的类,它是一个单例模式的应用。

1 2 3 4 5 6 7 8 9 10 11 12 13

public class AppSettings { private Context mAppContext; private static AppSettings sInstance = new AppSettings(); //some other codes public static AppSettings getInstance() { return sInstance; } public final void setup(Context context) { mAppContext = context; } }

当我们传入Activity作为Context参数时,则AppSettings实例会持有这个Activity的实例。

当我们旋转设备时,Android系统会销毁当前的Activity,创建新的Activity来加载合适的布局。如果出现Activity被单例实例持有,那么旋转过程中的旧Activity无法被销毁掉。就发生了我们所说的内存泄漏。

想要解决这个问题也不难,那就是使用Application的Context对象,因为它和AppSettings实例具有相同的生命周期。这里是通过使用Context.getApplicationContext()方法来实现。所以修改如下

1 2 3 4 5 6 7 8 9 10 11 12 13

public class AppSettings { private Context mAppContext; private static AppSettings sInstance = new AppSettings(); //some other codes public static AppSettings getInstance() { return sInstance; } public final void setup(Context context) { mAppContext = context.getApplicationContext(); } }

忘记反注册监听器

在Android中我们会使用很多listener,observer。这些都是作为观察者模式的实现。当我们注册一个listener时,这个listener的实例会被主题所引用。如果主题的生命周期要明显大于listener,那么就有可能发生内存泄漏。

以下面的代码为例

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); NetworkManager.getInstance().registerListener(this); } @Override public void onNetworkUp() { } @Override public void onNetworkDown() { } }

上述代码处理的业务,可以理解为

  • AppCompatActivity实现了OnNetworkChangedListener接口,用来监听网络的可用性变化
  • NetworkManager为单例模式实现,其registerListener接收了MainActivity实例

又是单例模式,可知NetworkManager会持有MainActivity的实例引用,因而屏幕旋转时,MainActivity同样无法被回收,进而造成了内存泄漏。

对于这种类型的内存泄漏,解决方法是这样的。即在MainActivity的onDestroy方法中加入反注销的方法调用。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); NetworkManager.getInstance().registerListener(this); } @Override public void onNetworkUp() { } @Override public void onNetworkDown() { } @Override protected void onDestroy() { super.onDestroy(); NetworkManager.getInstance().unregisterListener(this); } }

非静态内部类导致的内存泄漏

在Java中,非静态内部类会隐式持有外部类的实例引用。想要了解更多,可以参考这篇文章细话Java:”失效”的private修饰符

通常情况下,我们会书写类似这样的代码

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

public class SensorListenerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE); sensorManager.registerListener(new SensorListener() { @Override public void onSensorChanged(int sensor, float[] values) { } @Override public void onAccuracyChanged(int sensor, int accuracy) { } }, SensorManager.SENSOR_ALL); } }

其中上面的SensorListner实例是一个匿名内部类的实例,也是非静态内部类的一种。因此SensorListner也会持有外部SensorListenerActivity的实例引用。

而SensorManager作为单例模式实现,其生命周期与Application相同,和SensorListner对象生命周期不同,有可能间接导致SensorListenerActivity发生内存泄漏。

解决这种问题的方法可以是

  • 使用实例变量存储SensonListener实例,在Activity的onDestroy方法进行反注册。
  • 如果registerListener方法可以修改,可以使用弱引用或者WeakHashMap来解决。

除了上面的三种场景外,Android的内存泄漏还有可能出现在以下情况

  • 使用Activity.getSystemService()使用不当,也会导致内存泄漏。
  • 资源未关闭也会造成内存泄漏
  • Handler使用不当也可以造成内存泄漏的发生
  • 延迟的任务也可能导致内存泄漏

解决内存泄漏

想要解决内存泄漏无非如下两种方法

  • 手动解除不必要的强引用关系
  • 使用弱引用或者软引用替换强引用关系

下面会简单介绍一些内存泄漏检测和解决的工具

Strictmode

  • StrictMode,严格模式,是Android中的一种检测VM和线程违例的工具。
  • 使用detectAll()或者detectActivityLeaks()可以检测Activity的内存泄漏
  • 使用setClassInstanceLimit()可以限定类的实例个数,可以辅助判断某些类是否发生了内存泄漏
  • 但是StrictMode只能检测出现象,并不能提供更多具体的信息。
  • 了解更多关于StrictMode,请访问Android性能调优利器StrictMode

Android Memory Monitors

Android Memory Monitor内置于Android Studio中,用于展示应用内存的使用和释放情况。它大致长成这个样子

当你的App占用的内存持续增加,而且你同时出发GC,也没有进行释放,那么你的App很有可能发生了内存泄漏问题。

LeakCanary

  • LeakCanary是一个检测Java和Android内存泄漏的库
  • 由Square公司开发
  • 集成LeakCanary之后,只需要等待内存泄漏出现就可以了,无需认为进行主动检测。
  • 关于如何使用LeakCanary,可以参考这篇文章 Android内存泄漏检测利器:LeakCanary

Heap Dump

  • 一个Heap dump就是某一时间点的内存快照
  • 它包含了某个时间点的Java对象和类信息。
  • 我们可以通上述提到的Android Heap Monitor进行Heap Dump,当然LeakCanary也会生成Heap Dump文件。
  • 生成的Heap Dump文件扩展名为.hprof 即Heap Profile.
  • 通常情况下,一个heap profile需要转换后才能被MAT使用分析。

Shallow Heap VS Retained Heap

  • Shallow Heap 指的是对象自身的占用的内存大小。
  • 对象x的Retained Set指的是如果对象x被GC移除,可以释放总的对象的集合。
  • 对象x的Retained Heap指的就是上述x的Retained Set的占用内存大小。

以上图做个例子,进行分析

  • A,B,C,D四个对象的Shallow Heap均为1M
  • B,C,D的Retained Heap均为1M
  • A的Retained Heap为4M

真实情况下如何计算泄漏内存大小

上述的Retained Heap的大小获取是基于假设的,而现实在进行分析中不可能基于这种方法,那么实际上计算泄漏内存的大小的方法其实是这样的。

这里我们需要一个概念,就是Dominator Tree(统治者树)。

  • 如果对象x统治对象y,那么每条从GC根节点到y对象的路径都会经过x,即x是GC根节点到y的必经之路。
  • 上述情况下,我们可以说x是y的统治者
  • 最近统治者指的是离对象y最近的统治者。

上图中

  • A和B都不无法统治C对象,即C对象被A和B的父对象统治
  • H不受F,G,D,E统治,但是受C统治
  • F和D是循环引用,但是按照路径的方向(从根节点到对象),D统治F

内存泄漏与OOM

  • OOM全称Out Of Memory Error 内存溢出错误
  • OOM发生在,当我们尝试进行创建对象,但是堆内存无法通过GC释放足够的空间,堆内存也无法在继续增长,从而完成对象创建请求,所以发生了OOM
  • OOM发生很有可能是内存泄漏导致
  • 但是并非所有的OOM都是由内存泄漏引起
  • 内存泄漏也并不一定引起OOM

声明

  • 其中第一张图片GC回收图来自Patrick Dubroy在Google IO的演讲Keynote
  • 最后一张Dorminator Tree来自MAT官方网站

一些链接

最后的话

内存泄漏在App中很常见,需要我们花时间去解决。

处理内存泄漏问题,不仅要解决掉,更应该善于整理总结,做到后续编码中主动避免。

本文是我在droidcon beijing 2016和 GDG Beijing Devfest所做分享的文章总结版。如有问题,欢迎指出。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏xingoo, 一个梦想做发明家的程序员

【插件开发】—— 8 IPreferenceStore,插件的键/值存储!

前文回顾: 1 插件学习篇 2 简单的建立插件工程以及模型文件分析 3 利用扩展点,开发透视图 4 SWT编程须知 5 SWT简单控件的使用与布局搭...

22550
来自专栏求索之路

Android源码设计模式解析与实战笔记

1.单一职责原则:比如说一个ImageLoader,需要加载图片的缓存图片,此时如果将这两个功能都放在一个类中,就违反了这个原则, 我们需要将不同的功能用类精...

46750
来自专栏天天P图攻城狮

深入Android Runtime: 指令优化与Java方法调用

在进行apk热修复、插件化、动态加载的时候,会经常多个jar/dex包含相同的class,如果class结构因为需要升级出现了变化,会隐藏一些很难解释的坑在里面...

58660
来自专栏java一日一条

架构设计基础知识整理

From http://msdn.microsoft.com/en-us/library/ff647859.aspx

8220
来自专栏Android点滴积累

SharedPreferences 详解(多进程,存取数组解决方案)

一、SharedPreferences基本概念 文件保存路径:/data/data/<包名>/shared_prefs目录下目录下生成了一个SP.xml文件 S...

36390
来自专栏JAVA高级架构

适配器模式(Adapter)

10430
来自专栏developerHaoz 的安卓之旅

Android 关于内存泄露,你必须了解的东西

内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄...

7410
来自专栏Android开发指南

13.缓存、三级缓存、内存溢出、AsyncTask

449120
来自专栏Java呓语

DataBinding·常用注解说明

Observable接口提供给开发者添加/移除监听者的机制。为了使开发更便捷,我们创建了BaseObservable类,它已经实现了Observable接口中的...

14740
来自专栏码匠的流水账

聊聊spring cloud gateway的ForwardedHeadersFilter

本文主要研究一下spring cloud gateway的ForwardedHeadersFilter

10320

扫码关注云+社区

领取腾讯云代金券