Android内存泄漏全总结

什么是内存泄漏?

Android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。但是当对象不再被应用程序使用,仍然被生命周期长的对象引用,垃圾回收器无法回收。

内存泄露的根本原因:不再被使用的对象,因为一些不当的操作导致其被gc root持有无法被回收,最终内存泄漏。

常见的作为gc root的对象
开发里排查内存泄漏涉及比较多的gc root是:

静态引用、活动的线程

哪些情况会造成内存泄漏?
  • 错误使用单例造成的内存泄漏
  • Handler造成的内存泄漏
  • 线程造成的内存泄漏
  • 非静态内部类创建静态实例造成的内存泄漏
  • 资源未关闭造成的内存泄漏
  • 内部类造成的内存泄漏
静态成员引起内存泄漏

测试内部类持有外部类引用,内部类是静态的(GC-ROOT,将一直连着这个外部类实例)。

public class OutterClass {
    private String name;

    class Inner{
        public void list(){
            System.out.println("outter name is " + name);
        }
    }
}
public class OuterInnerActivity extends ComponentActivity {

    // 静态的内部类
    private static OutterClass.Inner inner;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        OutterClass outterClass = new OutterClass();
        inner = outterClass.new Inner();
    }
}
静态变量导致的内存泄露
public class OuterInnerActivity extends ComponentActivity {

    private static Context context;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //静态变量导致内存泄漏
        context = this;
    }
}

声明static后,sContext的生命周期将和Application一样长,Activity即使退出到桌面,Application依然存在->sContext依然存在,GC此时想回收Activity却发现Activity仍然被sContext(GC-ROOT连接着),导致死活回收不了,内存泄露。

单例模式导致的内存泄露
public class DownloadManager2 {
    private static DownloadManager2 instance;
    private List<DownloadListener> mListeners = new ArrayList<>();

    public interface DownloadListener {
        void done();
    }

    public static DownloadManager2 getInstance(){
        if (instance == null) {
            instance = new DownloadManager2();
        }
        return instance;
    }

    public void register(DownloadListener downloadListener){
        if (!mListeners.contains(downloadListener)) {
            mListeners.add(downloadListener);
        }
    }

    public void unregister(DownloadListener downloadListener){
        if (mListeners.contains(downloadListener)) {
            mListeners.remove(downloadListener);
        }
    }
}
public class StaticLeakActivity extends ComponentActivity implements DownloadManager2.DownloadListener{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //单例导致内存泄漏
        DownloadManager2.getInstance().register(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // 忘记 unregister
//         DownloadManager2.getInstance().unregister(this);
    }

    @Override
    public void done() {

    }
}
模拟Handler造成的内存泄漏情景:

Handler的生命周期和Activity可能不一致:当Activity销毁时,消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler是内部类,持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

public class ThirdActivity extends AppCompatActivity {
    ...
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d("fish", "hello world");
        }
    };
}

在Java里,匿名内部类默认持有外部类引用,并且此处编译器会有提示:HandlerLeak。
推荐使用静态类来继承Handler,因为使用匿名内部类可能会有内存泄漏风险。

修复方法:

  1. 使用静态内部类实现Handler功能,静态内部类默认没有持有外部类引用。
  2. 使用弱引用WeakReference
    3.在Activity退出的时候调用removeCallbacksAndMessages移除消息队列中所有消息和所有的Runnable。

项目中使用Handler可以写一个统一的Handler使用。

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new MyHandler().sendEmptyMessageDelayed(2, 5000);
    }

    static class MyHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d("fish", "hello world");
        }
    }
}

将内部类与外部类代码编译为字节码可以看到,
内部类与匿名内部类的实例都有一个外部类类型的名为this$0的变量指向了外部类的对象,所以是持有外部类的引用。当内部类使用static修饰,就没有这个指向外部类对象的变量。

为什么非静态内部类持有外部类引用,静态内部类不持有外部引用。
这个问题非常简单,就像 static的方法只能调用static的东西,非static可以调用非static和static的一样。
静态内部类调不了外部类的成员变量了。

既然匿名内部类会发生泄漏,那为啥还需要匿名内部类呢?

无需重新定义新的具名类
符合条件的匿名内部类可以转为Lambda表达式,简洁
匿名内部类可以直接访问外部类引用

Handler 泄漏的本质原因

构造Handler对象时会绑定当前线程的Looper,Looper里持有MessageQueue引用
当前线程的Looper存储在Thread里的ThreadLocal
当Handler发送消息的时候,构造Message对象,而该Message对象持有Handler引用
Message对象将会被放置在MessageQueue里
由此推断,Thread将会间接持有Handler,而Handler又持有外部类引用,最终Thread将会间接持有外部类引用,导致了泄漏。

参考:
https://www.cnblogs.com/ldq2016/p/6626670.html

线程造成的内存泄漏

线程如果在Activity中使用匿名内部类,那么它对当前Activity都有一个隐式引用。如果Activity在销毁之前,线程任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
}

这种线程导致的内存泄露问题应该如何解决呢?

有两种方式:

第一种:使用静态内部类替换匿名内部类
此种方式同Handler处理类似。

第二种:使用Lambda替换匿名内部类
代码:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        new Thread(() -> {
            try {
                Thread.sleep(200000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
}

Lambda表达式没有隐式持有外部类,因此此种场景下不会有内存泄漏风险。

注册不当内存泄漏

模拟一个简单下载过程,首先定义一个下载管理类:

public class DownloadManager {
   private DownloadManager() {
   }
   static class Inner {
      private static final DownloadManager ins = new DownloadManager();
   }
   public static DownloadManager getIns() {
      return Inner.ins;
   }
   private HashMap<String, DownloadListener> map = new HashMap();
   //模拟注册
   public void download(DownloadListener listener, String path) {
      map.put(path, listener);
      new Thread(() -> {
         //模拟下载
         listener.onSuc();
      }).start();
   }
}

interface DownloadListener {
   void onSuc();
   void onFail();
}

外部传入下载路径,下载成功后通知外界调用者:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        DownloadManager.getIns().download(new DownloadListener() {
            @Override
            public void onSuc() {
                //更新UI
            }
            @Override
            public void onFail() {
            }
        }, "hello test");
    }
}

因为需要在下载回调时更新UI,因此选择匿名内部类接收回调,而因为该匿名内部类DownloadListener被静态变量: DownloadManager.ins 持有。也就是说:
静态变量作为gc root,间接持有匿名内部类,最终持有Activity导致了泄漏。

如何规避此种场景下的内存泄漏呢?

1.静态内部类持有Activity弱引用
2.DownloadManager提供反注册方式,当Activity销毁时反注册从Map里移除回调

资源未关闭造成内存泄漏

无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。
忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。

使用RxBus等注册监听没有解除注册
Subscrib subscrib = RxBus.getDefault().subscribe(this, "XXX", new RxBus.Callback<String>() {
            @Override
            public void onEvent(String s) {
             
            }
        });
subscrib.unSubscrib();
匿名内部类、Lambda表达式是否存在泄漏问题
  • Java匿名内部类


。显然匿名内部类构造函数形参里有外部类的类型,当构造匿名内部类时会传递进去并赋值给匿名内部类的成员变量。

  • Java的Lambda是否会泄漏
    不会。Java Lambda并没有生成Class文件,而是通过INVOKEDYNAMIC 指令动态生成Runnable对象,最后传入Thread里。
    可以看出,此时生成的Lambda并没有持有外部类引用。
    若在Lambda内显式持有外部类引用,那么此时和Java 匿名内部类类似的,当外部类销毁的时候,如果Lambda被gc root 持有(间接/直接),那么将会发生内存泄漏

  • Kotlin匿名内部类会导致泄漏吗?
    示例:线程持有匿名内部类对象:

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread(object : Runnable {
            override fun run() {
                println("hello world")
            }
        }).start()
    }
}

编译Kotlin 匿名内部类字节码:


不会。Kotlin 匿名内部类没有隐式持有外部类引用。
若在Kotlin 匿名内部类内显式持有外部类引用,那么此时和Java 匿名内部类类似的,当外部类销毁的时候,如果Lambda被gc root 持有(间接/直接),那么将会发生内存泄漏。

  • Kotlin的Lambda是否会泄漏?
    线程持有Lambda对象:
class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread { println("hello world ") }
    }
}

结果和上面Java lambda的描述一致。

内存泄漏总结

总结:只有Java的匿名内部类隐式持有外部类引用,有内存泄漏风险,其他的写法不会造成内存泄漏。


总结
  1. 如果某些单例需要使用到Context对象,推荐使用Application的context,不要使用Activity的context,否则容易导致内存泄露。单例对象的生命周期和Application一致,这样Application和单例对象就一起销毁。

  2. 优先使用静态内部类而不是非静态的,因为非静态内部类持有外部类引用可能导致垃圾回收失败。如果你的静态内部类需要宿主Activity的引用来执行某些东西,你要将这个引用封装在一个WeakReference中,避免意外导致Activity泄露,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。

参考:
https://mp.weixin.qq.com/s/09dowu8FOON5pyOf3Y69WA
https://blog.csdn.net/weixin_61845324/article/details/131786085

Github 参考demo地址:

https://github.com/running-libo/PerformanceOpt

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

推荐阅读更多精彩内容