Android —— 内存泄漏检查

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android —— 内存泄漏检查相关的知识,希望对你有一定的参考价值。

今天地铁上看到一篇不错的将内存泄漏简单检查的文章,觉得还不错哟,内存泄漏确实是每个程序员头疼的事情,这里就多研究一下咯^^

一. 常见的垃圾回收算法

参看文章

引用计数法
引用计数法基本上最简单的垃圾回收策略,它的核心思想是:
当有指针指向某实例时,计数加一, 当删除一个指针时,计数减一,当计数为0时,说明该实例没有引用可以被垃圾回收器回收。
这种回收策略的缺点是显而易见的:
1.维护引用计数是有开销的
2.计数的保存会消耗额外的空间
3.无法处理循环引用

标记清除
标记清除,顾名思义分为2步:1.标记 2.清除。标记清除会先从根扫描所有的可达对象,不可达的对象就是无用的垃圾对象。然后回收器集中清除垃圾对象。这种策略的缺点是: 1.标记整理的时候需要JVM停止其它工作 2.整理后产生了很多内存碎片

复制算法
复制算法的核心是:将内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存区间中的存活对象复制到另一块,之后,清除正在使用的一块中的所有对象,然后两块区域交换角色。缺点:
1.将内存折半,利用率低

标记压缩算法
标记压缩算法在标记清除算法的基础上做了些改进,它在标记完所有的有效对象后,将有效的内存压缩到一起,然后再清除其它区域。

分代算法
因为每一种算法都有各自的优略,所以根据对象的不同特点使用不同的回收算法能够显著提高垃圾回收的效率。分代算法正是基于这种思想产生的,像上一篇我们介绍的Java堆,一般有年轻代和老年代。年轻代的特点就是对象的存活时间短,所以这个时候选用复制算法是非常合适的。 而老年代中的对象一般不会被回收,这个时候如果也用复制算法显然是不合适的, 这个时候JVM选择了标记压缩算法。
对于年轻代和老年代来说,一般年轻代的垃圾回收频率高耗时短,老年代的回收频率低耗时长。为了应对这种情况,JVM使用了一种卡表的数据结构。其中记录某一区域的老年代对象是否持有年轻代对象的引用,如果没有持有的话就不需要扫描老年代,可以加大年轻代的回收效率。

分区算法
分代算法按照对象的生命周期将对象分为了两部分,而分区算法将整个堆空间划分成不同的区域,每一个区域都独立使用,独立回收。这种算法的好处是可以控制一次回收的区域个数,这样可以根据目标的停顿时间选择合适的回收个数,从而达到减少GC停顿的目的。

二.Java的4种引用方式与GC的关系

参看博文
任何被强引用指向的对象都不能被回收;
弱引用在GC时一定会被回收;
软引用只有在内存不足时才会被回收;
虚引用在任何时刻都可能会被回收,程序中可通过判断引用队列中是否已经加入虚引用来判断对象是否将要被GC。

来看下面三组例子:

public class ReferenceTest {
WeakReference w;

public void test() {
    w = new WeakReference<String>(new String("aaa"));
    System.gc();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(w.get());
}

}
答案是null,因为”aaa”这个对象只有w这一个弱引用指向它。

String a = new String(“aaa”);
w = new WeakReference(a);
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(w.get());
答案是”aaa”,因为”aaa”被一个强引用a指向,所以GC时不会被回收,因此w仍到得到这个对象。

String a = new String(“aaa”);
w = new WeakReference(a);
a = null;
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(w.get());
答案是null,因为”aaa”起初被一个强引用a指向,但后来这个强引用没了,所以GC时”aaa”就被回收了。

三.内存泄漏

虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。这些对象积累在内存中,直到程序结束,就是我们所说的“内存泄漏”。
这里我们来区别一下内存抖动:
内存抖动
在极短的时间内,分配大量的内存,然后又释放它,这种现象就会造成内存抖动。典型地,在 View 控件的 onDraw 方法里分配大量内存,又释放大量内存,这种做法极易引起内存抖动,从而导致性能下降。因为 onDraw 里的大量内存分配和释放会给系统堆空间造成压力,触发 GC 工作去释放更多可用内存,而 GC 工作起来时,又会吃掉宝贵的帧时间 (帧时间是 16ms) ,最终导致性能问题。

Q1:在android开发测试中一般如何发现内存泄漏的发生呢?

答:
方法1:反复操作观察内存变化

内存泄漏常见变现为程序使用时间越长,内存占用越多。那我们通过反复操作应用,比如反复点开/关闭页面,观察内存变化状况是否一点点上涨,可以粗略地判断是否有内存泄漏。观察哪里呢?
1)可以查看DDMS中的heap。
技术分享
2)在Android Studio中可以通过查看Android Monitor窗口
技术分享

注:内存抖动和内存泄漏 对于内存波动的区别:
技术分享
技术分享

内存泄漏 我们通常可以利用 MAT 工具进行专业分析,这个我们后面来讲解。
方法2:通过代码检测Activity泄漏 利用LeakCanary来检测
LeakCanary 是一个用来检查 Android 下内存泄漏的开源库。不错的介绍文章

如果有一个工具能自动完成这些事情,甚至在发生 OOM 之前,就把内存泄漏报告给你,那是多么美好的一件事情啊。LeakCanary 就是用来干这个事情的。在测试你的 App 时,如果发生了内存泄漏,状态栏上会有通知告诉你。logcat 上也会有相应的 log 通知你。哇塞,酷毙了!既然是开源,那我们直接来看看如何使用吧。
1)集成 LeakCanary 库(在Android Studio)
在Studio中添加依赖,可以通过project structure,不要忘了设置debugCompile和releaseCompile ,最终将会在gradle中的依赖中多了这几行代码。

dependencies {
    debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.4‘
    releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4‘
}

设置debug版和release版的原因:
在 debug 版本上,集成 LeakCanary 库,并执行内存泄漏监测,而在 release 版本上,集成一个无操作的 wrapper ,这样对程序性能就不会有影响。
2)初始化LeakCanary

  • 我们需要继承Application,在onCreate()里调用LeakCanary.install(this)。LeakCanary.install() 返回一个配置好了的 RefWatcher 实例。
public class ExampleApplication extends Application {

    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication)   context.getApplicationContext();
        return application.refWatcher;
    }

    private RefWatcher refWatcher;

    @Override public void onCreate() {
        super.onCreate();
        refWatcher = LeakCanary.install(this);
    }
}
  • 然后,在AndroidManifest.xml注册下。
 <application
    android:name=".ExampleApplication "
    android:allowBackup="true"
    android:icon="@mipmap/ic_logo"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">

3)监控Activity泄漏
我们经常把 Activity 当作为 Context 对象使用,在不同场合由各种对象引用 Activity。所以,Activity 泄漏是一个重要的需要检查的内存泄漏之一。 在Application中,我们调用LeakCanary.install(this)。 这个方法返回一个配置好了的 RefWatcher 实例。它同时安装了 ActivityRefWatcher 来监控 Activity 泄漏。所以,当我们初始化之后就可以自动监控 Activity 泄露,即当 Activity.onDestroy() 被调用之后,如果这个 Activity 没有被销毁,logcat 就会打印出信息告诉你内存泄漏发生。LeakCanary 的第一次分析可能会耗时较久,耐心等待。

4)监控 Fragment 泄漏

public abstract class BaseFragment extends Fragment {

    @Override 
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher =     ExampleApplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
    }
}

当 Fragment.onDestroy() 被调用之后,如果这个 fragment 实例没有被销毁,那么就会从 logcat 里看到相应的泄漏信息。

5)更多
LeakCanary 自动检测 Activity 泄漏只支持 Android ICS 以上版本。因为 Application.registerActivityLifecycleCallbacks() 是在 API 14 引入的。如果要在 ICS 之前监测 Activity 泄漏,可以重载 Activity.onDestroy() 方法,然后在这个方法里调用 RefWatcher.watch(this) 来实现。

  • 上代码来看一下吧
    我创建了两个activity,在第一个Activity中点击按钮进入SecondAcitivity,在SecondAcitivity中创建了匿名内部类,而且持有一个静态的变量,这样,恭喜,内存泄漏就离你不远了。如果要详细了解可能发生内存泄漏的几种可能,请查看博主的下篇博文喽。
    来来来 ,上代码:
    MainAcitivy:
public class MainActivity extends AppCompatActivity {
    private int i = 99;
    private Button btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn = (Button) findViewById(R.id.button);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(),SecondActivity.class);
                startActivity(intent);
            }
        });

    }
}

SecondActivity:

public class SecondActivity extends Activity {
    private static int index = 0;
    private static Object inner;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_second);
        createInnerClass();
        for (int i = 0; i < 40; i++) {
//            Log.i("dongmj","hello");
            index++;
        }
    }
    void createInnerClass() {
        class InnerClass {
        }
        inner = new InnerClass();
    }
}

// 查看打印的log
技术分享
显然leakcanary提示你内存泄漏喽,而且已经精确到时由于SecondAsitivity中的inner类造成的,哇哦,,神奇的不得了。
更加神奇的是,在手机应用中会出现leaks的应用名称,打开后显示的是你的应用出现内存溢出的时间,并且可以点击查看详情。
技术分享

4 Hprof文件

在上面例子中我们发现log中提示,在sd卡中生成了hprof文件,根据这个文件你可以对内存泄漏进行分析。
如何导出:

File heapDumpFile = new File("heapdump.hprof");
Debug.dumphprofData(heapDumpFile.getAbsolutePath());

如何分析 hprof 文件

这是个比较大的话题,感兴趣的可以移步另外一个开源库 HAHA,它的祖先是 MAT。



































































以上是关于Android —— 内存泄漏检查的主要内容,如果未能解决你的问题,请参考以下文章

Android中的内存泄漏和内存溢出

Android中的内存泄漏和内存溢出

如何使用 Malloc Debug 来检查本机内存泄漏?

如何使用模块化代码片段中的LeakCanary检测内存泄漏?

Android ValueAnimator --内存泄漏

Android —— 内存泄漏检查