What is a memory leak on Android, exactly?
onDestroy() is the standard example. Something still points at it, so it stays.
A leak does not crash on the spot. It fails no test and raises no lint warning. The same page describes how it ends: "As small memory leaks accumulate, memory usage grows, the Garbage Collector (GC) runs more frequently and consumes more CPU, causing jank, UI freezes and Application Not Responding (ANR) reports, eventually leading to an OutOfMemoryError (OOME) crash."
The earlier version of this article opened with statistics: 22% of stability issues in the top 1,000 apps, a median of 4.7 minutes from leak to crash, a percentage for each cause of leaks, and a finding attributed to two named companies. None of them had a source. The page linked for the first one does not contain it. All of them are removed.
One figure does have a source, and it is about one app: "When Square engineers first enabled LeakCanary in the Square Point Of Sale app, they were able to fix several leaks and reduced the OOM crash rate by 94%."
The documented common causes are three, all lifecycle bugs:
1. "Adding a Fragment instance to the backstack without clearing that Fragment's view fields in Fragment.onDestroyView()".
2. "Storing an Activity instance as a Context field in an object that survives activity recreation due to configuration changes."
3. "Registering a listener, broadcast receiver or RxJava subscription which references an object with lifecycle, and forgetting to unregister when the lifecycle reaches its end."LeakCanary: Automatic Leak Detection in Debug Builds
// build.gradle.kts (app module)
dependencies {
// debugImplementation: LeakCanary should only run in debug builds
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// Confirm it is running: filter Logcat on the LeakCanary tag.
// D LeakCanary: LeakCanary is running and ready to detect leaks
// Watching an object LeakCanary does not know about:
class ImageCache : Closeable {
override fun close() {
// After close(), nothing should hold this object
AppWatcher.objectWatcher.watch(this, "ImageCache was closed")
}
}Reading Leak Traces Like a Detective
Leaking: YES, NO or UNKNOWN, and underlines the references it suspects with ~~~.
The narrowing is deduction from what LeakCanary knows about lifecycles, and the guide walks through it on the trace below. Two facts do the work. "In an Android app the Application instance is a singleton that is never garbage collected", so the reference into it is not the cause. "Views should not survive the lifecycle of their context", so the TextView is leaking and the reference out of it is not the cause either. What stays underlined is the short list: three references, all on the path from the Application's list to the view.
One piece of advice from the earlier version of this article has to be withdrawn. Both the text and the FAQ suggested a WeakReference as a fix. LeakCanary's documentation says the opposite: "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." Clear the reference at the right point in the lifecycle instead.// A leak trace, from the LeakCanary documentation:
//
// ┬───
// │ 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
//
// The cause is the first underlined reference,
// ExampleApplication.leakedViews: an object that lives as long as
// the process, holding a view that belongs to one Activity.Android Studio Profiler: Heap Dumps
Common Leak Patterns and Fixes
remember block whose lambda captured a Context, described as holding the Activity "forever". No source supports that description and the pattern is removed. The fix in each case below is the same idea: end the reference when the lifecycle ends. For coroutines, a scope tied to the lifecycle does it for you. The reference describes lifecycleScope as a "CoroutineScope tied to this LifecycleOwner's Lifecycle" (Android), and the rules behind that are in the coroutines guide.// PATTERN 1: Fragment view fields kept after onDestroyView()
// BAD: the binding holds the whole view tree while the
// Fragment waits on the back stack
class ProfileFragment : Fragment() {
private var binding: FragmentProfileBinding? = null
// FIX: clear view fields when the view is destroyed
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
// PATTERN 2: an Activity stored in an object that outlives it
// BAD: the ViewModel survives a configuration change,
// the Activity it points at does not
class MainViewModel : ViewModel() {
lateinit var activity: Activity
}
// FIX: pass data down. Never store an Activity, a View or a
// Context from one in a ViewModel or a singleton.
// PATTERN 3: a listener that is never unregistered
// BAD: the singleton now holds the Fragment
object EventBus {
private val listeners = mutableListOf<EventListener>()
fun register(listener: EventListener) { listeners.add(listener) }
fun unregister(listener: EventListener) { listeners.remove(listener) }
}
class MyFragment : Fragment(), EventListener {
override fun onStart() {
super.onStart()
EventBus.register(this)
}
// FIX: unregister at the matching end of the lifecycle
override fun onStop() {
super.onStop()
EventBus.unregister(this)
}
}Production Monitoring: What You Can Actually See
ActivityManager.getHistoricalProcessExitReasons returns "a list of ApplicationExitInfo records containing the reasons for the most recent app deaths" (reference). One of the reasons is REASON_LOW_MEMORY. Count those on the next launch and send the count to your analytics.
2. Run LeakCanary in release, knowingly. There is an artifact for it, leakcanary-android-release, and its page carries a warning you should read before you ship it: "Everything about this is experimental."
The earlier version of this article recommended logging PSS every 60 seconds, alerting at 150% of a per-screen budget and enabling release detection for 1 to 5% of users. Those were our numbers with no measurement behind them, and they are removed. Low memory kills also feed the stability data Google Play shows you; the Android vitals guide covers how that is scored.// On launch: how many recent deaths were low memory kills?
fun lowMemoryKills(context: Context): Int {
val activityManager =
context.getSystemService(ActivityManager::class.java)
// pid 0 and maxNum 0 mean "do not filter": all records
val exits = activityManager.getHistoricalProcessExitReasons(
context.packageName, 0, 0
)
return exits.count {
it.reason == ApplicationExitInfo.REASON_LOW_MEMORY
}
}Key Takeaways
- 1A memory leak is a reference kept to an object that is no longer needed. Most are lifecycle bugs.
- 2LeakCanary 2.14 needs one debugImplementation line and watches Activities, Fragments, fragment Views, ViewModels and Services.
- 3It waits 5 seconds, then dumps the heap at 5 retained objects while the app is visible, or 1 when it is not.
- 4Read a leak trace top down and use the Leaking marks to rule references out.
- 5A WeakReference does not fix a leak. Clear the reference when the lifecycle ends.
- 6In production, count REASON_LOW_MEMORY exits. Out-of-memory crashes scatter across stack traces and low memory kills are not crashes at all.
Frequently Asked
How do I know if my app has a memory leak?
Add LeakCanary to the debug build and use the app. It watches destroyed Activities, Fragments, fragment Views, cleared ViewModels and destroyed Services, and reports any that are still retained after five seconds and a garbage collection. In production, count process exits with REASON_LOW_MEMORY, because a leaking app is often killed before it crashes.
What is the most common cause of a memory leak on Android?
LeakCanary documents three: a Fragment on the back stack that keeps its view fields after onDestroyView, an Activity stored as a Context in an object that survives configuration changes, and a listener or subscription that is never unregistered. All three are the same bug, a reference that outlives the lifecycle of what it points at.
Can I fix a memory leak with a WeakReference?
No. The LeakCanary documentation states that memory leaks cannot be fixed by replacing strong references with weak references and that the approach never works. The bug that kept the reference alive is still there, and objects can now be collected too early. Clear the reference at the correct point in the lifecycle.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.