Android性能优化:内存泄漏,只有知根才能知底

讲关于内存泄露之前,先抛出一个问题,两个相互引用的对象是不是一定会引起内存泄露?回答这个问题之前就需要理清内存泄露是怎么产生的。首先,分配了内存的对象是可达的,既可追溯到根节点的,其次,这个对象是没用的,既再也用不到这个对象。这个时候,垃圾收集器因为相关的对象是可达的,因此无法标记为垃圾,但它又没什么用,且占内存,因此这就是内存泄露。

我们是不是可以这么想,如果已经分配的对象无法追溯到根节点,既如果对应的根节点已经置空,是不是就可以被垃圾收集器标记了。因此,从源头根节点开始,查看是哪个引用了该对象,然后又因为根节点没有及时释放,从根节点开始,还存在有向图到该对象节点,那么这种情况必定存在内存泄露。因此解决的思路不就是,找到根节点,然后找到从根节点(包括根节点)下来各路的引用对象,查找可能没解决的,这样就可以快速找到泄露的原因了。实际上,一般都是因为根节点没有释放引起的。

既然涉及到根节点,那么就有必要了解下什么样的对象可以作为根节点?

在Java中,其实很好理解,有一下几类可以作为根节点:

  1. 虚拟机栈中(虚拟机栈中的本地变量表)引用的对象。如下列子, 在栈帧对一个的method01方法中,局部变量t既为根节点,在method01方法还没执行完成的情况下,t作为GC roots不会被回收,当method01执行栈帧返回时,局部变量跟随方法消失,此时进行GC既会回收没被t引用的对象。
public class TestGCRoots01 {
    private int _10MB = 10 * 1024 * 1024;
    private byte[] memory = new byte[8 * _10MB];
 
    public static void main(String[] args) {
        method01();
        System.out.println("返回main方法");
        System.gc();
        System.out.println("第二次GC完成");
    }
 
    public static void method01() {
        TestGCRoots01 t = new TestGCRoots01();
        System.gc();
        System.out.println("第一次GC完成");
    }
}
  1. 方法区中类静态属性应用的变量。例子如下,main方法执行完之后,因为t属于类静态变量,存于方法区中,所以执行GC之后,回收了t2引用的对象,而t引用的对象不会被回收,因为类静态属性t为根节点没被释放。
public class TestGCRoots02 {
    private static int _10MB = 10 * 1024 * 1024;
    private byte[] memory;
 
    private static TestGCRoots02 t;
 
    public TestGCRoots02(int size) {
        memory = new byte[size];
    }
 
    public static void main(String[] args) {
        TestGCRoots02 t2 = new TestGCRoots02(4 * _10MB);
        t2.t = new TestGCRoots02(8 * _10MB);
        t2 = null;
        System.gc();
    }
}
  1. 方法区中常量引用的对象,这一点同2中的类似;
  2. 本地方法栈中JNI(Native方法)中引用的对象,这一点同1中的类似。

上面是关于Java GC根节点,那么在Android中,又是哪些可以作为GC root对象呢?,以下根节点都有对应的table来遍历

  1. 被加载到虚拟机的系统类对象;
  2. 在虚拟机内部创建的原子类,如Double、Boolean类等。
  3. 线程和线程块引用的对象
  4. JNI本地变量和JNI全局全局引用的对象
  5. 位于虚拟机栈线程所引用的对象
  6. 等待finalizer运行的垃圾可回收对象
  7. 有一个finalize方法,但还没finalized和还没在finalizer队列上的对象
  8. 对象不可达,但是被MAT标记成root的对象
  9. Java栈帧
  10. 在虚拟机内部创建的OutOfMemory异常对象、Internal异常对象以及NoClassDefFoundError异常对象

知道了根节点的出处,我们查找内存泄露就事半功倍了,只要根节点被释放了,那么相关的引用对象才能得以释放,才不会发生内存泄露的问题。因此,知道什么是根节点是解决内存泄漏等垃圾回收重要的一个知识点。

单纯知道哪些是根节点只能在编码上尽量避免内存泄露,但在实际的开发生产过程中,我们经常会因为疏忽等而发生内存泄露问题。那么如何查看哪些引用的对象存在内存泄露了,这个时候我们就需要借助工具帮我们查找了。

现在查找内存泄露的工具很多,笔者比较懒,只想一个Android Studio就能查出问题所在。因此,自然就想到用Profile工具,实际上,这个Profile太好用了,你可以大体知道,Java的堆是如何慢慢增大的、哪里发生卡顿了、哪里网络请求比较差、可以具体到哪一步哪一个函数占用了比较多的时间等等。

这里,我们只讲如何借助Profile查找内存泄露的。打开profile工具(工具怎么使用可以谷歌百度),首先进入预判发生内存泄露的页面,然后退出页面,然后点击左边红圈的图标,表示进行垃圾回收,执行完成之后,点击右边红圈的图标,进行dump相关依然存在的heap。通过查看相关的heap中有没有还存在已退出页面相关的引用对象存在,存在的话就可能会发生内存泄露了。

image

笔者在开发过程中,遇到一个泄露日志是LazDetailActivity存在泄露的风险,于是通过接着Profile,dump的结果如下,通过查找关键字,得到已退出的页面LazDetailActivity仍然存在,相关的TaoLiveVideoView也仍然存在。

image

接着说明如何查找内存泄露问题。根据上图dump下来的,笔者把问题瞄准到对应的listener,一般情况,我们都习惯registerListener和ungRegisterListener配套,那么问题来了,是不是只用regiseterListener而没调用unRegisterListener就一定会发生内存泄露了,其实不然,正如我们上面分析的,只要根节点能释放,那么就可以不用。而为什么还要ungRegisterListener呢?原因之一就是可能在registerListener挂钩的根节点没有得到释放,那么通过来ungRegisterListener保证可以顺利被回收。而在查找过程当中,发现reisterListener都是正常注册到对象成员变量中,不存在所谓的根节点。接着,查看相关的类有没有类静态属性、发现也没有。接着,查看到本地代码中有块代码如下:

private void sendMsg(Message msg) {
    Log.i(TAG, this + "\tsendMsg");
    synchronized (mHandlerLock) {
        if (mHandler == null || mThread == null || !mThread.isAlive() || mHandler.getLooper() == null) {
            Log.d(TAG, this + "\tplay thread not ready, create...");
            mThread = new HandlerThread("lazvideo_play");
            mThread.start();
            mHandler = new PlayHandler(this, mThread.getLooper());
        }
        mHandler.sendMessage(msg);
    }
}

这块代码引起了我的关注,起初我一开始认定泄露应该是从这里引起的,一,HandlerThread启动了线程在跑,满足作为根节点(参考上面根节点),二还包含了Handler,当我以为问题很容易就解决的时候,我查看了下PlayHandler,结果该Handler是静态的,且对this加了弱引用,因此,在垃圾回收的时候,不存在回收不了this指向的对象问题。之所以找到这一步,一是遵从查找根对象,二是因为我们SDK提供给第三方业务接入的时候,因为我们架构设计不完善的问题,我们有两个类分别需要用到释放资源的,这两个类中释放资源的方法最终都会调用到上面方法sendMsg,只不过其中一个会对mThread、looper进行关闭,一个可能不小心又创建了线程,正是因为调用方可能存在方法调用顺序的不同,导致这个线程没有关闭。然而,这个并不是导致内存泄露问题,只是线程没停止而已。最后,把问题瞄准了Handler,在另一个类中,执行了Handler handler = new Handler(this)。显然,这块是潜在存在内存泄露的,message.target持有handler对象。然后查看OnDestroy看看有没有做释放,看到Handler置空了,那么这个置空是否就已经没问题了,其实仍然存在内存泄露的风险,因为在发送延时消息的时候,message已经持有callback这个对象了,且target已经指向handler所在的对象。那么只是置空handler,并不能保证handler指向的对象没有其他引用引用,因此还需调用handler.removeCallbacksAndMessages(null)这个方法,把消息指向的callback都清空了。

通过这次的内存泄露查找,我们可以总结查找内存泄露是有规律可循的,就像垃圾回收如何收集对象一样。垃圾收集器一开始也要比较哪些是根节点,所以我们要先确认的是哪些是根节点,在页面退出的时候,根节点是否释放了。确认根节点下来的引用是否引用了View或Activity,因为引用了View,其实就间接了引用了Activity,而Activity是页面的对象容器。因此我们往往看到的泄露也是和Activity相关。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容