androidengineers.Book a session

Performance & Internals

Debugging Memory Leaks

article45 minHard

A memory leak is an object that should have been garbage collected but is still referenced. In Android, the most common victim is an Activity or Fragment — large objects that hold views, bitmaps, and often a whole screen worth of state.

A single leaked Activity can retain tens of megabytes. Repeated navigation leaks accumulate until the system kills the process.

What Causes Leaks

Static references. A static field holding a context or view will outlive the Activity.

// Leaked: companion object holds Activity context forever
companion object {
    var context: Context? = null
}

Anonymous callbacks registered but never unregistered. This is the most common leak pattern.

// Leaked: listener holds a reference to the Activity/Fragment
sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent) {
        updateUI(event) // holds Activity reference
    }
}, sensor, SensorManager.SENSOR_DELAY_UI)
// Never unregistered in onStop/onDestroy

Coroutines launched in the wrong scope. Coroutines launched in GlobalScope or a manually created scope that is never cancelled will keep running and may hold references to UI objects.

// Leaked: Activity is destroyed but coroutine still runs and holds a reference
GlobalScope.launch {
    delay(10_000)
    textView.text = "Done"
}

Always use viewModelScope or lifecycleScope.

Inner classes. Non-static inner classes in Java (and equivalent in Kotlin) hold an implicit reference to the outer class.

LeakCanary

LeakCanary is a leak detection library that automatically watches Activities, Fragments, and ViewModels. When it detects an object that should have been GC'd but wasn't, it takes a heap dump and traces the reference chain.

Add it only to the debugImplementation configuration — it should never ship in a release build:

debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")

No code changes needed. LeakCanary hooks into the Activity lifecycle automatically. When a leak is detected, it shows a notification with the full reference chain.

Reading a LeakCanary Report

A typical report looks like:

┬───
│ GC Root: Thread
│
├─ HandlerThread
│    Thread.contextClassLoader
├─ PathClassLoader
│    PathClassLoader.runtimeInternalObjects
├─ LoginActivity instance
│    LoginActivity.networkCallback
└─ NetworkCallback instance

Read from top to bottom. The GC root is why the chain is alive. The bottom is the leaked object. The path in between is what needs to be fixed. In this case, a NetworkCallback is holding a reference to LoginActivity through a field. Fix: unregister the callback in onStop or use lifecycleScope.

Memory Profiler for Manual Investigation

When you cannot use LeakCanary or need to investigate a complex case:

  1. Navigate to the suspicious screen.
  2. Navigate away.
  3. Click Force GC in the Memory Profiler.
  4. Take a heap dump.
  5. Filter for Activity or Fragment in the class list.
  6. If instances exist after navigation, they are likely leaked.
  7. Expand the references tree to find what is holding them.

WeakReference as a Fix

When a callback must reference a UI object, hold a WeakReference so the object can be GC'd if the UI is gone.

class MyCallback(activity: MainActivity) : SomeCallback {
    private val activityRef = WeakReference(activity)

    override fun onResult(data: String) {
        activityRef.get()?.updateUI(data)
    }
}

This is safer than a direct reference but not a substitute for proper lifecycle management. Prefer lifecycleScope over manual WeakReference when possible.

ViewModel Leaks

ViewModels are lifecycle-aware but they can still leak if they hold a direct reference to a View or Context.

// Bad: ViewModel holding a View reference
class BadViewModel : ViewModel() {
    var textView: TextView? = null // holds Activity context → leak
}

ViewModels should only hold data and business objects, never Views or Activity contexts. If you need application context, inject it via ApplicationContext, not Activity context.

Common Fixes

CauseFix
Listener not unregisteredUnregister in onStop or use lifecycle-aware API
GlobalScope coroutineUse lifecycleScope or viewModelScope
Static context referenceUse ApplicationContext or remove the static field
Inner class callbackMake it a top-level class or use WeakReference
ViewModel holding a ViewRemove the reference; observe state in the UI layer

Practice

Add LeakCanary to a project. Navigate between screens that pass context objects as parameters. Let LeakCanary run for a few minutes and inspect any reports. Fix the first leak it reports by tracing the reference chain and removing or weakening the problematic reference.

Summary

Memory leaks in Android are almost always caused by strong references that outlive the component they were created in. LeakCanary finds them automatically. The Memory Profiler exposes them manually. Fix leaks by using lifecycle-aware scopes, unregistering callbacks, and never holding View or Activity references in long-lived objects.

YOUR LEARNING JOURNEY

0 of 17 available lessons completed

Progress saved in this browser. No account needed.
Debugging Memory Leaks | Senior Android Developer | Android Engineers