修复内存泄漏
内存泄漏是一种编程错误,它导致应用程序保留了对不再需要的对象的引用。在代码的某个地方,存在一个应该被清除但未清除的引用。
按照这 4 个步骤来修复内存泄漏
- 找到泄漏路径。
- 缩小嫌疑引用范围。
- 找到导致泄漏的引用。
- 修复泄漏。
LeakCanary 帮你完成前两个步骤。最后两个步骤取决于你自己!
1. 找到泄漏路径¶
泄漏路径是从垃圾收集根到被保留对象的最佳强引用路径的简称,即在内存中持有对象从而阻止其被垃圾回收的引用路径。
例如,我们将一个 helper 单例存储在一个静态字段中
class Helper {
}
class Utils {
public static Helper helper = new Helper();
}
我们告诉 LeakCanary 该单例实例预期会被垃圾回收
AppWatcher.objectWatcher.watch(Utils.helper)
该单例的泄漏路径如下所示
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper
让我们分解一下!在顶部,一个 PathClassLoader
实例被一个垃圾收集 (GC) 根持有,更具体地说,是一个本地代码中的局部变量。GC 根是总是可达的特殊对象,即它们不能被垃圾回收。GC 根主要有 4 种类型
- 局部变量,属于线程的栈。
- 活跃 Java 线程的实例。
- 系统类,永不卸载。
- 本地引用,由本地代码控制。
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
以 ├─
开头的行表示一个 Java 对象(可以是类、对象数组或实例),而以 │ ↓
开头的行表示对下一行 Java 对象的引用。
PathClassLoader
有一个 runtimeInternalObjects
字段,它是对一个 Object
数组的引用
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
该 Object
数组中位置 43 的元素是对 Utils
类的引用。
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
以 ╰→
开头的行表示泄漏对象,即传递给 AppWatcher.objectWatcher.watch() 的对象。
Utils
类有一个静态的 helper
字段,它是对泄漏对象的引用,即 Helper 单例实例
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper instance
2. 缩小嫌疑引用范围¶
泄漏路径是一系列引用组成的路径。最初,该路径中的所有引用都被怀疑是导致泄漏的原因,但 LeakCanary 可以自动缩小嫌疑引用范围。为了理解这意味着什么,我们手动走一遍这个过程。
这里有一个糟糕的 Android 代码示例
class ExampleApplication : Application() {
val leakedViews = mutableListOf<View>()
}
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val textView = findViewById<View>(R.id.helper_text)
val app = application as ExampleApplication
// This creates a leak, What a Terrible Failure!
app.leakedViews.add(textView)
}
}
LeakCanary 会生成一个看起来像这样的泄漏路径
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ ↓ ExampleApplication.leakedViews
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
├─ java.lang.Object[] array
│ ↓ Object[].[0]
├─ android.widget.TextView instance
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
以下是阅读该泄漏路径的方法
FontsContract
类是一个系统类(参见GC Root: System class
),它有一个sContext
静态字段,该字段引用了一个ExampleApplication
实例,该实例有一个leakedViews
字段,该字段引用了一个ArrayList
实例,该实例引用了一个数组(作为 ArrayList 实现的后备数组),该数组包含一个元素,该元素引用了一个TextView
,该TextView
有一个mContext
字段,该字段引用了一个已销毁的MainActivity
实例。
LeakCanary 使用 ~~~ 下划线高亮显示所有被怀疑导致此泄漏的引用。最初,所有引用都是嫌疑对象
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
│ ~~~~~~~~
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ ↓ TextView.mContext
│ ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance
然后,LeakCanary 对泄漏路径中对象的状态和生命周期进行推断。在 Android 应用中,Application
实例是一个永不被垃圾回收的单例,因此它永远不会泄漏(Leaking: NO (Application is a singleton)
)。由此,LeakCanary 得出结论,泄漏不是由 FontsContract.sContext
引起的(移除了相应的 ~~~
)。以下是更新后的泄漏路径
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ ↓ TextView.mContext
│ ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance
TextView
实例通过其 mContext
字段引用了已销毁的 MainActivity
实例。视图不应在其上下文的生命周期结束后仍然存活,因此 LeakCanary 知道此 TextView
实例正在泄漏(Leaking: YES (View.mContext references a destroyed activity)
),从而得出泄漏不是由 TextView.mContext
引起的(移除了相应的 ~~~
)。以下是更新后的泄漏路径
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
总结一下,LeakCanary 检查泄漏路径中对象的状态,以确定这些对象是否正在泄漏(Leaking: YES
对比 Leaking: NO
),并利用这些信息来缩小嫌疑引用范围。你可以提供自定义的 ObjectInspector
实现,以改进 LeakCanary 在你的代码库中的工作方式(参见识别泄漏对象和标记对象)。
3. 找到导致泄漏的引用¶
在前面的示例中,LeakCanary 将嫌疑引用范围缩小到 ExampleApplication.leakedViews
、ArrayList.elementData
和 Object[].[0]
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
ArrayList.elementData
和 Object[].[0]
是 ArrayList
的实现细节,ArrayList
实现中不太可能存在 bug,因此导致泄漏的引用是唯一剩下的引用:ExampleApplication.leakedViews
。
4. 修复泄漏¶
找到导致泄漏的引用后,你需要弄清楚该引用是什么,它何时应该被清除以及为何没有被清除。有时原因很明显,就像前面的示例一样。有时你需要更多信息才能弄清楚。你可以添加标签,或直接探索 hprof 文件(参见如何深入挖掘泄漏路径之外的信息?)。
警告
内存泄漏不能通过将强引用替换为弱引用来修复。这是一种试图快速解决内存问题的常见方法,但它永远无效。导致引用保留时间超过必要的 bug 仍然存在。除此之外,这还会引入更多 bug,因为某些对象现在会比预期更早被垃圾回收。这也使得代码更难维护。
接下来做什么?使用代码示例根据你的需要自定义 LeakCanary!