Fixing a memory leak
A memory leak is a programming error that causes an application to keep a reference to an object that is no longer needed. Somewhere in the code, there’s a reference that should have been cleared and wasn’t.
Follow these 4 steps to fix memory leaks:
- Find the leak trace.
- Narrow down the suspect references.
- Find the reference causing the leak.
- Fix the leak.
LeakCanary helps you with the first two steps. The last two steps are up to you!
1. Find the leak trace¶
A leak trace is a shorter name for the best strong reference path from garbage collection roots to the retained object, ie the path of references that is holding an object in memory, therefore preventing it from being garbage collected.
For example, let’s store a helper singleton in a static field:
class Helper {
}
class Utils {
public static Helper helper = new Helper();
}
Let’s tell LeakCanary that the singleton instance is expected to be garbage collected:
AppWatcher.objectWatcher.watch(Utils.helper)
The leak trace for that singleton looks like this:
β¬βββ
β 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
Let’s break it down! At the top, a PathClassLoader instance is held by a garbage collection (GC) root, more specifically a local variable in native code. GC roots are special objects that are always reachable, ie they cannot be garbage collected. There are 4 main types of GC root:
- Local variables, which belong to the stack of a thread.
- Instances of active Java threads.
- System Classes, which never unload.
- Native references, which are controlled by native code.
β¬βββ
β GC Root: Local variable in native code
β
ββ dalvik.system.PathClassLoader instance
A line starting with ββ represents a Java object (either a class, an object array or an instance), and a line starting with β β represents a reference to the Java object on the next line.
PathClassLoader has a runtimeInternalObjects field that is a reference to an array of Object:
ββ dalvik.system.PathClassLoader instance
β β PathClassLoader.runtimeInternalObjects
ββ java.lang.Object[] array
The element at position 43 in that array of Object is a reference to the Utils class.
ββ java.lang.Object[] array
β β Object[].[43]
ββ com.example.Utils class
A line starting with β°β represents the leaking object, ie the object that is passed to AppWatcher.objectWatcher.watch().
The Utils class has a static helper field which is a reference to the leaking object, which is the Helper singleton instance:
ββ com.example.Utils class
β β static Utils.helper
β°β java.example.Helper instance
2. Narrow down the suspect references¶
A leak trace is a path of references. Initially, all references in that path are suspected of causing the leak, but LeakCanary can automatically narrow down the suspect references. To understand what that means, let’s go through that process manually.
Here’s an example of bad Android code:
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 produces a leak trace that looks like this:
β¬βββ
β 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
Here’s how to read that leak trace:
The
FontsContractclass is a system class (seeGC Root: System class) and has ansContextstatic field which references anExampleApplicationinstance which has aleakedViewsfield which references anArrayListinstance which references an array (the array backing the array list implementation) which has an element that references aTextViewwhich has anmContextfield which references a destroyed instance ofMainActivity.
LeakCanary highlights all references suspected of causing this leak using ~~~ underlines. Initially, all references are suspect:
β¬βββ
β 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
Then, LeakCanary makes deductions about the state and the lifecycle of the objects in the leak trace. In an Android app the Application instance is a singleton that is never garbage collected, so it’s never leaking (Leaking: NO (Application is a singleton)). From that, LeakCanary concludes that the leak is not caused by FontsContract.sContext (removal of corresponding ~~~). Here’s the updated leak trace:
β¬βββ
β 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
The TextView instance references the destroyed MainActivity instance via it’s mContext field. Views should not survive the lifecycle of their context, so LeakCanary knows that this TextView instance is leaking (Leaking: YES (View.mContext references a destroyed activity)), and therefore that the leak is not caused by TextView.mContext (removal of corresponding ~~~). Here’s the updated leak trace:
β¬βββ
β 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
To summarize, LeakCanary inspects the state of objects in the leak trace to figure out if these objects are leaking (Leaking: YES vs Leaking: NO), and leverages that information to narrow down the suspect references. You can provide custom ObjectInspector implementations to improve how LeakCanary works in your codebase (see Identifying leaking objects and labeling objects).
The two rules behind the narrowing¶
The example above had one Leaking: NO and one Leaking: YES, so the remaining suspects were easy to see. Real leak traces are mostly Leaking: UNKNOWN, and what makes them tractable is that a single verdict tells you about more than one object:
- If an object should still be in memory, then every object above it should still be in memory.
- If an object should not be in memory, then every object below it should not be in memory either.
LeakCanary applies both rules itself, which is why one conclusion can update several lines at once, and why a status sometimes names another object instead of a field. Had there been more objects above ExampleApplication in the trace above, each of them would have been marked like this:
β Leaking: NO (ExampleApplicationβ is not leaking)
β means the reason is further down the trace, and β means it is further up. So ExampleApplicationβ is not leaking reads as this object is not leaking, because an object below it is not leaking, and everything above a non leaking object is non leaking too. A β status is the mirror image, produced by the second rule. When a rule overrules an inspector, the status says so, with a reason ending in Conflicts with ....
That leaves every leak trace in three parts, top to bottom: the objects known not to be leaking, the objects whose state is unknown, and the objects known to be leaking. The reference causing the leak is the one joining the first part to the last, so it is somewhere in the unknown middle β which is exactly what ~~~ marks.
Work from the middle, not from the top
When several objects in a row are Leaking: UNKNOWN, resist starting at one end. Because the two rules propagate in both directions, settling the state of an object in the middle of the unknown section removes about half of it whichever way the answer comes out. Ten unknown objects is around three questions, not ten.
3. Find the reference causing the leak¶
In the previous example, LeakCanary narrowed down the suspect references to ExampleApplication.leakedViews, ArrayList.elementData and 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 and Object[].[0] are implementation details of ArrayList, and it’s unlikely that there’s a bug in the ArrayList implementation, so the reference causing the leak is the only remaining reference: ExampleApplication.leakedViews.
4. Fix the leak¶
Once you find the reference causing the leak, you need to figure out what that reference is about, when it should have been cleared and why it hasn’t been. Sometimes it’s obvious, like in the previous example. Sometimes you need more information to figure it out. You can add labels, or explore the hprof directly (see How can I dig beyond the leak trace?).
The LeakCanary Method works all four steps through on a real leak in a third party SDK, including the part this page can’t cover: what to do when the leak trace runs out of answers and the root cause is somewhere else entirely.
Warning
Memory leaks cannot be fixed by replacing strong references with weak references. It’s a common solution when attempting to quickly address memory issues, however it never works. The bugs that were causing references to be kept longer than necessary are still there. On top of that, it creates more bugs as some objects will now be garbage collected sooner than they should. It also makes the code much harder to maintain.
What’s next? Learn about the design decisions behind LeakCanary!