V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
m30102
V2EX  ›  Android

关于 gc 和非静态内部类引起内存泄漏的疑问

  •  
  •   m30102 · 2019-11-19 14:03:08 +08:00 · 6114 次点击
    这是一个创建于 1591 天前的主题,其中的信息可能已经有所发展或是发生改变。
    public class TActivity extends Activity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // TODO 耗时任务
                }
            }).start();
            
            textView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                }
            });
        }
    }
    

    很多人说上面这个例子会造成 Activity 内存泄漏,原因是非静态匿名内部类持有外部类的引用。 那么问题来了

    1 必须是耗时任务的内部类才会造成泄漏吗?下面的 OnClickListener 也是非静态匿名内部类.
    2 第一次 gc 发生时耗时任务可能还在执行,Activity 此时没有被回收,如果第二次 gc 时 耗时任务结束了,此时 Activity 会被回收吗?如果不会原因是什么?如果会,客户端很少有退不出的耗时任务,也就是说前面泄漏的内存很可能会被后面的 gc 回收,那么多次泄漏的内存可能不会累加,所以客户端不用太担心什么泄漏问题吧?
    3 如果我把下面的点击监听改为这样, 在没有触发点击事件的情况下不可能会造成泄漏吧?

    textView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            // TODO 耗时任务
                        }
                    }).start();
    
                }
            });
    
    8 条回复    2019-11-20 14:53:57 +08:00
    wshcdr
        1
    wshcdr  
       2019-11-19 14:12:57 +08:00
    关注这个问题
    find2bHusky
        2
    find2bHusky  
       2019-11-19 14:58:47 +08:00
    1.生命周期长的对象引用生命周期短的对象就会造成生命周期短的对象内存泄漏。在 Activity 调用 onDestory()的之后,Activity 对象就应该能被 gc 回收,onDestory()调用之后,textView 的回调也就取消了。但是线程有可能还没结束,所以会造成内存泄漏。
    2.对象应该被回收而没有被回收就是内存泄漏。但是对象什么时候应该被回收,gc 是不知道的,需要开发者去分析,Leakcanary 就是根据 Activity 的生命周期是否结束,来判断 Activity 是否被回收来确定是否有内存泄漏。内存泄漏一般会导致 OOM,我还没有遇到过内存泄漏的 err。有耗时任务,最好不要持有 Activity 的强引用,让 Activity 及时被回收。
    3.没有触发点击事件,Thead 对象没有执行,所以 Thead 对象会被回收,也就不会再持有 Activity 的引用。
    m30102
        3
    m30102  
    OP
       2019-11-19 17:02:21 +08:00
    @find2bHusky
    不知道我理解的对不对。
    gc 不是应该判定对象是否能到达 gcroots 而选择回收吗? run 方法执行完之前, run 的栈算是 gcroots, run 的栈引用着 activity, 所以 activity 算是到 gcroots 可达从而没被回收?
    run 方法执行完之后, run 方法的栈会被释放, 内部类虽然还是引用着 activity,但不存在 gcroots 了,activity 此时应该就没有可达的 gcroots

    所以关于第二点疑问, 已经泄露的 activity 能否在后面的 gc 中被回收还是不太明白.
    shily
        4
    shily  
       2019-11-19 17:31:04 +08:00   ❤️ 2
    1. 如#2 所说 『生命周期长的对象引用生命周期短的对象就会造成生命周期短的对象内存泄漏。』
    如果 Thread 不需要 Activity,但因为 Runnalbe 是一个非静态内部类,会默认持有外部类的引用;非主观期望,就是泄露。
    如果 Thread 需要 Activity,我就是启动一个空的 Activity,来执行,也不占用大量的资源,主观期望,就不是泄露了。

    2. 循环依赖(持有环:TActivity->textView->$OnClickListener->TActivity ),GC 是能发现和处理循环依赖,依然可以进行回收;
    如果你把 textView 对象传递给外部的对象,比如一个单例对象持有,就会导致整个环均无法被回收。

    3. 没有点击的情况下,Thread 对象和 匿名的 Runnable 对象均未创建,当然没有泄露。
    在此场景下,假设点击行为发生,持有链为 $Runnable -> $OnClickListener-> Activity。

    需要注意:
    类与对象的关系,针对第三种场景,就是类有依赖,但对象未创建。
    我们讨论内存泄露时,考虑的是对象间的引用关系,对象间的引用关系是由类来表达的。


    关于 #3
    不是栈,是引用链,方法栈会导致引用关系,但不是一一对应。

    方法完成后,Runnble 对象已经没有其他对象引用了,可以被回收,进而它所引用的 activity 等资源也可以被回收。

    已经泄露的对象,可以通过切断持有关系来让 GC 回收。以 OnClickListener 为例,如果我在 onCreate 中把 textView 放到一个单例的对象中持有,那么这个 TActivity 就会有一条引用关系 Root-> Singleton->textView->$OnClickListener->activity,导致 TActivity 泄露。

    如果是:
    public final class Singleton {
    private Singleton() {

    }

    public static final Singleton INSTANCE = new Singleton();

    public View holder;
    }

    那么,下一次再次进入 TActivity 时,新的 textView 对象被赋予 holder 时,原持有的 textView 和 原 TActivity 可以被回收。
    m30102
        5
    m30102  
    OP
       2019-11-19 18:22:25 +08:00
    @shily 谢谢,每一点解释的很清晰,我再多看两遍 ^_^
    pdog18
        6
    pdog18  
       2019-11-19 19:36:07 +08:00
    @shily 老哥讲的很好,有个地方可能笔误了,看下是不是。
    ————————
    3. 没有点击的情况下,Thread 对象和 匿名的 Runnable 对象均未创建,当然没有泄露。
    在此场景下,假设点击行为发生,持有链为 $Runnable -> $OnClickListener-> Activity。
    ————————
    这里应该是 Runnable$1 -> Activity,中间没有 Listener。





    ——————————————————
    已经泄露的对象,可以通过切断持有关系来让 GC 回收。以 OnClickListener 为例,如果我在 onCreate 中把 textView 放到一个单例的对象中持有,那么这个 TActivity 就会有一条引用关系 Root-> Singleton->textView->$OnClickListener->activity,导致 TActivity 泄露。

    ————————————
    这个引用链其实应该遵循最短路径,而最短路径应该在 textView 的时候直接通过他的 Context 也就是 Activity 引用到了,虽然你这样说也没有错,对理解的确会有帮助,但是感觉这样会更加“准确”一点。
    Root -> Singleton -> textView -> activity
    shily
        7
    shily  
       2019-11-19 22:08:19 +08:00
    @pdog18
    第一个,是没有问题的,Runnable 是 OnClickListener 的内部类,所以引用关系是 $Runnable -> $OnClickListener-> Activity,检查一下生成的类可以看到引用的是 TActivity$1 即 匿名的 OnClickListener 类。

    确实如你所说,分析内存泄漏时,一般优先分析最短路径,textView 的内部通过 mContext 已经引用了 Activity。
    此非最短路径,分析和解决问题时均需要考虑到,为了释放 activity 引用,这两个引用链均需要在合适的时机置空。(其实是在所有的情况下都不应该把 View 放置在全局引用)
    pdog18
        8
    pdog18  
       2019-11-20 14:53:57 +08:00
    @shily 你说的对! 查看了一下生成的类,Runnable 里面是引用到 OnClickListener 而不是直接引用到 Activity
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3160 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:31 · PVG 20:31 · LAX 05:31 · JFK 08:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.