彻底搞懂 Java 内存泄露
简书 编程之乐
Java 内存回收方式
Java 判断对象是否可以回收使用的而是可达性分析算法。
在主流的商用程序语言中 (Java 和 C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 (Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,下图对象 object5, object6, object7 虽然有互相判断,但它们到 GC Roots 是不可达的,所以它们将会判定为是可回收对象。
在 Java 语言里,可作为 GC Roots 对象的包括如下几种:
摘自《深入理解 Java 虚拟机》
使用 leakcanary 检测泄漏
关于 LeakCanary 使用参考以下文章:
- LeakCanary: 让内存泄露无所遁形
- LeakCanary 中文使用说明
LeakCanary 的内存泄露提示一般会包含三个部分:
第一部分 (LeakSingle 类的 sInstance 变量)
引用第二部分 (LeakSingle 类的 mContext 变量),
导致第三部分 (MainActivity 类的实例 instance) 泄露.
leakcanary 使用注意
即使是空的 Activity,如果检测泄露时候遇到了如下这样的泄露,注意,把 refWatcher.watct() 放在 onDestroy 里面即可解决,或者忽略这样的提示。
由于文章已写很多,下面的就不再修改,忽略这种错误即可。
leakcanary 和代码示例说明内存泄露
案例一 (静态成员引起的内存泄露)
测试内部类持有外部类引用,内部类是静态的 (GC-ROOT, 将一直连着这个外部类实例),静态的会和 Application 一个生命周期,这会导致一直持有外部类引用 (内部类隐含了一个成员变量 $0), 即使外部类制空 = null,也无法释放。
OutterClass
TestActivity
案例二 (单例模式引起的内存泄露)
DownloadManager
Task
Call
TestActivity
部分日志打印如下: 当前的 GC_ROOT 是 DownloadManager 的 instance 实例。
关于上面两种方式导致的内存泄露问题,这里再举两个案例说明以加强理解。
案例三 (静态变量导致的内存泄露)
打印日志如下:
从这段日志可以分析出:声明 static 后,sContext 的生命周期将和 Application 一样长,Activity 即使退出到桌面,Application 依然存在 ->sContext 依然存在,GC 此时想回收 Activity 却发现 Activity 仍然被 sContext(GC-ROOT 连接着),导致死活回收不了,内存泄露。
上面的代码改造一下,如下。
日志如下
案例四 (单例模式导致的内存泄露)
DownloadManager
TestActivity
错误写法一定导致内存泄露吗?
答案是:否定的。
如下案例,有限时间内是可以挽救内存泄露发生的,所以控制好生命周期,其他情况:如单例对象 (静态变量) 的生命周期比其持有的 sContext
的生命周期更长时 -> 内存泄露,更短时 -> 躲过内存泄露。
Handler 引起的内存泄露详细分析
handler 导致的内存泄露可能我们大多数都犯过.
注意以下代码中的注释, 非静态内部类虽然持有外部类引用,但是持有并不代表一定泄露,而是看 gc 时谁的命长。经过测试 情况 (1) 始终没有内存泄露。
为什么会这样, 很早阅读 Handler 源码时候记得这几个货都是互相引用来引用去的,Message 有个 target 字段, message.target = handler;
handler.post(message); 又把这个 message 推入了 MessageQueue 中,而 MessageQueue 是在一个 Looper 线程中不断轮询处理消息,而有时候 message 还是个老不死,能够重复利用。如果当 Activity 退出时候,还有消息未处理或正在处理,由于 message 引用 handler,handler 又引用 Activity,此时将引发内存泄露。
知道了原理后,即使写出易于内存泄露的代码也能够避免内存泄露啦。
上述代码只需在 onDestroy() 函数中调用 mHandler.removeCallbacksAndMessages(null);
在 Activity 退出的时候的移除消息队列中所有消息和所有的 Runnable。
内部类引起的内存泄露
内部类种类大致如下:
- 非静态内部类 (成员内部类)
- 静态内部类 (嵌套内部类)
- 局部内部类 (定义在方法内或者作用域内的类, 好似局部变量, 所以不能有访问控制符和 static 等修饰)
- 匿名内部类 (没有名字, 仅使用一次 new 个对象即扔掉类的定义)
为什么非静态内部类持有外部类引用, 静态内部类不持有外部引用。
这个问题非常简单,就像 static 的方法只能调用 static 的东西,非 static 可以调用非 static 和 static 的一样。static–> 针对 class, 非 static-> 针对 对象,我是这么简单理解的。看图:
匿名内部类
将局部内部类的使用再深入一步,假如只创建某个局部内部类的一个对象,就不必命名了。
匿名内部类的类型可以是如下几种方式。
- 接口匿名内部类
- 抽象类匿名内部类
- 类匿名内部类
匿名内部类总结:
- 其实主要就是类定义一次就失效了,主要使用的是这个类 (不知道名字) 的实例。根据内部类的特性,能够实现回调和闭包。
- JavaScript 和 Python 的回调传递的是 fuction,Java 传递的是 object。
Java 中常常用到回调,而回调的本质就是传递一个对象,JavaScript 或其他语言则是传递一个函数 (如 Python,或者 C, 使用函数指针的方式), 由于传递一个对象可以携带其他的一些信息,所以 Java 认为传递一个对象比传递一个函数要灵活的多 (当然 java 也可以用 Method 反射对象传递函数)。参考《Java 核心技术》
非静态内部类导致内存泄露 (前提 dog 的命长)
下面的案例将导致内存泄露
其中
- private static Dog dog ;
导致 Dog 的命比 TestActivity 长,这就糟糕了,但是注意,如果改为 - private Dog dog ;
即使 Dog 是非静态内部类,也不会导致内存泄露,要死也是 Dog 先死, 毕竟 Dog 是 TestActivity 的家庭成员,开挂也得看主人。
哪些内部类或者回调函数是否持有外部类对象?
一个反射案例说明一切
上述结果足够说明,即使是方法内的回调对象也是持有外部类引用的,那么虽然作用域是局部的,也有存在内存泄露的可能。我多次强调 持有某对象 不代表一定泄露,看的是谁命长。回调在 Android 开发过程中几乎处处可见,如果持有就会内存泄露的话,那几乎就没法玩了。
一般情况下,我们常常设置某个方法内的
- xx.execute(new Listener(){xx});
是不会导致内存泄露的,这个方法执行完,局部作用域就失效了。但是如果在 execute(listener); 过程中,某个单例模式的对象 突然引用了这个 listener 对象,那么就会导致内存泄露。
下面用实例证明我的想法
TestActivity
Task
DownloadManager
这足以证明我的想法是正确的,Callback 的不巧当使用同样会导致内存泄露 的发送。
总结
- 如果某些单例需要使用到 Context 对象,推荐使用 Application 的 context,不要使用 Activity 的 context,否则容易导致内存泄露。单例对象的生命周期和 Application 一致,这样 Application 和单例对象就一起销毁。
- 优先使用静态内部类而不是非静态的, 因为非静态内部类持有外部类引用可能导致垃圾回收失败。如果你的静态内部类需要宿主 Activity 的引用来执行某些东西,你要将这个引用封装在一个 WeakReference 中,避免意外导致 Activity 泄露, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收 只被弱引用关联 的对象,只被 说明这个对象本身已经没有用处了。
来源: https://wangli0.github.io/性能优化/彻底搞懂Java内存泄露/