Android内存泄漏分析

779 阅读7分钟

一、Java内存结构

Java是在JVM所虚拟出的内存环境中运行的,Java虚拟机在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,这些区域都有各自的用途、创建时间、销毁时间。

栈(stack):栈中只存放基本类型和对象的引用(不是对象),虚拟机栈存放Java变量引用,本地方法栈存放native变量引用。

堆(heap): 堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。堆中不存放基本类型和对象引用,只存放对象本身。

方法区(method): 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

程序计数器: 可以看做是当前线程执行的字节码的行号指示器,内存空间是线程私有的。

二、什么是内存泄露?

内存泄露: ML (Memory Leak),程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。换种说法:GC是不会回收gc roots对象的,Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地被gc roots对象引用,也就是可达gc roots对象,导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏。

在java中可以作为GC Roots的对象有以下几种: 虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象。 内存泄漏对程序的影响是:容易使得应用程序发生内存溢出,即 OOM 。

内存溢出: OOM(Out of Memory),程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现OOM。

三、Android中常见的内存泄露

四、Android内存泄露分析解决

下面将会介绍4种中内存泄露的分析方式,大家只要关注最后两种就好,重点是最后一种MAT工具分析方式。

1)使用adb命令,检测context引用不当引发的内存泄露

某种全局性的操作或者某个内部类持有了Activity的引用,并且一直不去释放它,导致Activity也一直无法释放,这使得GC也无法回收这个部分的内存,最终导致内存泄露。 举个例子,这里使用Handler引起内存泄露:

public class SecondActivity extends AppCompatActivity {
    private Handler handler = new Handler();
    private MyRunnable myRunnable = new MyRunnable();
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            handler.postDelayed(this,10000);
        }
    }
 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_two);
        handler.postDelayed(myRunnable,10000);
}

在Terminal窗口输入命令查看内存使用情况:

adb shell dumpsys meminfo <packageName>

首次进入应用,只有一个主Activity,这时点击按钮跳转到SecondActivity,输入命令,窗口显示如下:

依旧显示为了两个Activity,重复操作,多次打开关闭SecondActivity,会发现,每次打开都会新增一个。通过adb命令可以清楚的知道context引起的内存泄露。

2)使用Android Profile定位出问题的代码

依然使用1)中的例子,经由Android Profile定位;进入退出SecondActivity三次,然后点击Android Profile中的按钮:

先点击回收按钮,然后点击第二个按钮,检测当前Java堆,选择按包名排序,得到结果如下:

点击SecondActivity,右侧弹出界面:

会发现有三个SecondActivity实例,这个肯定是有问题的,SecondActivity被什么东西持有了,退SecondActivity时实例没有得到释放。继续点击SecondActivity,得到结果如下:

可以很清晰的知道是myRunnable实例持有了SecondActivity的实例,造成了内存泄露。 最后处理这个Handler引起的内存泄露:重写Activity的onDestroy()方法清空handler的队列。

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (handler != null){
            handler.removeCallbacks(myRunnable);
        }
}

3)LeakCanary

Square 公司出品的内存分析工具,接入方式也比较简单,见官方地址: github.com/square/leak… 当内存泄漏发生时,LeakCanary 会弹窗提示并生成对应的堆存储信息记录,或者执行命令启动Activity展示泄露的信息:

adb shell am start -n com.example.qiluo.mytest/com.squareup.leakcanary.internal.DisplayLeakActivity

这让我们对隐蔽的内存泄漏问题有了更加直观的感觉,但从实际使用来看,LeakCanary 的每个提示也并非是真正存在内存泄漏问题,要想确定是否存在问题我们还需要借助 MAT 来进行最后的确定。

优点

  • 接入方式简单,前期开发过程中就可以尽早接入,早些发现问题。
  • Activity展示疑似泄露的调用栈信息比较直观。

缺点

  • 生成分析dump文件比较费时。
  • 通过监听ActivityLifecyleCallbacks的生命周期检测,所以只能检测到activity的内存泄漏,比如service 的内存泄漏就检测不到。

4)使用MAT定位出问题的代码(强烈推荐)

首先从android studio导出hprof文件,然后需要使用以下命令转化下,才能被MAT识别:

/Users/qiluo/Library/Android/sdk/platform-tools/hprof-conv xxx.hprof new.hprof

Android SDK提供了这个工具 hprof-conv (位于 sdk/platform-tools下) 转化完成后,就可以使用MAT工具打开文件new.hprof了。

在全面掌握MAT的用法前,必须先了解下面的一些概念:

with outgoing references和with incoming references的区别:

with outgoing references:该对象引用了哪些对象。 with incoming references:哪些对象引用了该对象。

Shallow Heap和Retained Heap的区别: Shallow Heap:该对象自身占用的内存。 Retained Heap:还包含该对象引用的所有对象所占用的内存。

除此之外,MAT共有5个关键组件帮助我们去分析内存方面的问题,他们分别是Dominator_tree 、Histogram、thread_overview、Top Consumers、Leak Suspects。下面我们简单地了解一下它们。

Dominator(支配者): 如果从GC Root到达对象A的路径上必须经过对象B,那么B就是A的支配者。

Histogram和dominator_tree的区别:

Histogram显示Shallow Heap、Retained Heap、Objects,而dominator_tree显示的是Shallow Heap、Retained Heap、Percentage。 Histogram基于类的角度,dominator_tree是基于实例的角度。Histogram不会具体显示每一个泄漏的对象,而dominator_tree会。

thread_overview: 查看有多少线程和线程的Shallow Heap、Retained Heap、Context Class Loader与is Daemon。

熟练使用MAT,需要学会以下几个操作:

  • 善于使用Regex查找具体的类。
  • 善于使用group by package查找对应包下的具体类。
  • 善于使用group by class结合Regex查找对应包下的具体类。
  • 善于使用OQL(Object Query Language) 查找满足条件的所有对象。

在MAT中支持OQL对象查询语句,这是一个类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有的对象。OQL将内存中的每一个不同的类当做一个表,类的对象则是这表中的一条记录,每个对象的成员变量则对应着表中的一列。

下面列出两句比较常用的语法:

SELECT * FROM "com.*mytest..*"(查找该包名下的所有类)
SELECT * FROM INSTANCEOF android.app.Activity(查找Activity的所有子类)

MAT是非常强大的内存分析工具,强烈推荐使用,具体的操作暂时先不做演示,更多操作使用方法见如下链接:

blog.csdn.net/aaa2832/art…

blog.csdn.net/yxz32913095…