Skip to content
advanced Phase 11 · Performance & Optimization

Memory Leaks

Detect and fix memory leaks with LeakCanary, Android Profiler, and common leak patterns.

50m
2 problems
Topic Progress 0%

What Are Memory Leaks?

What Are Memory Leaks?

A memory leak occurs when an object is no longer needed but cannot be garbage collected because another object still holds a strong reference to it. On Android, this means the GC cannot reclaim memory, leading to increased RAM usage, jank, and eventually an OutOfMemoryError.

Why Memory Leaks Are Critical on Android

Mobile devices have limited RAM compared to desktops. Android's process lifecycle is aggressive about killing background processes to free memory. A leaking app:

  • Consumes more RAM than necessary
  • Triggers more frequent GC pauses, causing UI jank
  • Gets killed sooner by the system
  • May crash with OOM in extreme cases

The Android Memory Model

Android uses a generational garbage collector. Objects are allocated in the young generation and promoted to the old generation if they survive multiple GC cycles. Memory leaks create objects that persist in the old generation because a long-lived reference prevents collection.

App allocates object → Young Gen → Survives GC → Old Gen → LEAK: Cannot be collected

LeakCanary Setup

LeakCanary is the standard tool for detecting leaks in debug builds:

// build.gradle.kts (app module)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.13")
}

No initialization code is needed. LeakCanary hooks into the Application class automatically via ContentProvider. When an Activity or Fragment is destroyed, LeakCanary waits five seconds, then triggers a GC and checks if the weak references have been cleared. If not, it dumps the heap and analyzes it.

The LeakCanary UI

When LeakCanary detects a leak, it shows a notification. Tapping it opens a detailed screen showing:

  • The leaking object (e.g., MainActivity)
  • The reference chain from GC roots to the leaking object
  • The path that prevents garbage collection

This reference chain is the key to diagnosing the leak. You follow the chain to find the earliest strong reference that should have been removed.

Common Leak Patterns

1. Static references to Activity or Context

// LEAK: Static reference holds Activity alive
object AppManager {
    var currentActivity: Activity? = null
    
    fun onActivityResumed(activity: Activity) {
        currentActivity = activity
    }
}

When the user rotates the screen, the old Activity is destroyed but the static reference keeps it alive. The fix is to use WeakReference or clear the reference in onPause.

2. Inner classes and anonymous listeners

// LEAK: Non-static inner class holds reference to outer class
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // This anonymous Runnable holds a reference to MyActivity
        val handler = Handler(Looper.getMainLooper())
        handler.postDelayed(object : Runnable {
            override fun run() {
                // Activity reference is held here
                doSomething()
            }
        }, 60_000)
    }
}

The anonymous Runnable is an inner class that implicitly holds a reference to MyActivity. If the Activity is destroyed before the 60-second delay, it leaks. The fix: use a static inner class with a WeakReference, or use a lifecycle-aware component.

3. Coroutines tied to Activity scope

// LEAK: Coroutine outlives the Activity
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            val result = withContext(Dispatchers.IO) {
                // Long-running network call
                api.fetchData()
            }
            // This runs after Activity may be destroyed
            updateUI(result)
        }
    }
}

Use repeatOnLifecycle or flowWithLifecycle to tie the collection to the lifecycle. This way, the coroutine is cancelled when the Activity reaches STOPPED state.

4. Unregistered listeners and callbacks

// LEAK: Listener not unregistered
class SensorActivity : AppCompatActivity() {
    private lateinit var sensorManager: SensorManager
    
    override fun onResume() {
        super.onResume()
        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
    }
    
    // BUG: Missing unregisterListener in onPause
}

Always pair registerListener with unregisterListener, and registerReceiver with unregisterReceiver. Use try-finally or lifecycle-aware components to guarantee cleanup.

Detecting and Diagnosing Leaks

Android Profiler

Android Studio's Memory Profiler gives you real-time visibility into your app's memory usage.

Opening the Profiler:

  1. Run your app in debug mode
  2. View > Tool Windows > Profiler
  3. Click the Memory tab

The profiler shows:

  • Allocations per second: High allocation rates may indicate churn
  • GC frequency: Frequent GC means memory pressure
  • Heap size: Growing heap after rotation indicates a leak

Using the Profiler to Detect Leaks:

  1. Perform the suspect action (e.g., rotate the screen)
  2. Click the GC button to force garbage collection
  3. Take a heap dump
  4. Search for the destroyed Activity by class name
  5. Inspect the reference chain

LeakCanary in Detail

LeakCanary provides structured leak traces. Here's how to read one:

┬───
│ GC Root: Thread object
│
├─ android.os.HandlerThread instance
│    Leaking: NO (MessageQueue is not null)
│    Thread name: 'LeakCanary-HeapDumper'
│    ↓ HandlerThread.mLooper
├─ android.os.Looper instance
│    ↓ Looper.mQueue
├─ android.os.MessageQueue instance
│    ↓ MessageQueue.mMessages
├─ android.os.Message instance
│    ↓ Message.target
├─ android.os.Handler instance
│    ↓ Handler.mCallback
├─ com.example.MyActivity$$ExternalSyntheticLambda1 instance
│    Leaking: YES (ObjectWatcher was watching this)
│    ↓ MyActivity$$ExternalSyntheticLambda1.f$0
╰─ com.example.MyActivity instance

Key things to note:

  • Leaking: YES marks the object that should have been collected
  • Leaking: NO marks objects that are still in use
  • Follow the chain upward from the leaking object to find the problematic reference

Common Leak Trace Patterns

Pattern 1: Handler postDelayed

Activity → Handler → MessageQueue → Message → Runnable → Activity

Fix: Use Handler(Looper.getMainLooper()).postDelayed with a lifecycle-aware wrapper.

Pattern 2: WebView

Activity → ContextThemeWrapper → mBase → WebView → ... → Activity

WebViews are notorious for leaking. Destroy them explicitly in onDestroy():

override fun onDestroy() {
    webView.apply {
        stopLoading()
        destroy()
    }
    super.onDestroy()
}

Pattern 3: Coroutine scope

ViewModel → viewModelScope → Job → Activity

Fix: Never hold Activity references in ViewModel. Use LiveData or StateFlow to communicate back.

Automated Leak Testing

You can write tests that fail when leaks are detected:

@RunWith(AndroidJUnit4::class)
LeakTest {
    @Test
    fun noLeaksAfterRotation() {
        val activity = rule.activity
        rule.activity.recreate()
        System.gc()
        Thread.sleep(5000)
        val ref = WeakReference(activity)
        assertThat(ref.get()).isNull()
    }
}

LeakCanary also exposes a test helper: ObjectWatcher can be used programmatically to watch objects and assert they are collected.

Production Monitoring

In production, you won't have LeakCanary. Instead:

  • Use Firebase Crashlytics to track OOM crashes
  • Monitor onTrimMemory() callbacks to understand memory pressure
  • Track ANR rates, as memory pressure often contributes to ANRs
  • Use Android Vitals in Google Play Console for memory insights

Fixing and Preventing Leaks

The Principle of Shortest Reference Lifetime

The root cause of most leaks is holding a reference longer than the object's intended lifetime. Follow this rule: every reference should have a lifetime equal to or shorter than the object it references.

Context Leaks

Android has three types of Context:

  • Application Context: Lives as long as the app process
  • Activity Context: Lives as long as the Activity
  • Service Context: Lives as long of the Service

Use the lightest Context that works:

// Use Application context for singletons
object DatabaseHelper {
    private lateinit var db: RoomDatabase
    
    fun init(context: Context) {
        // Use context.applicationContext to avoid Activity leak
        db = Room.databaseBuilder(
            context.applicationContext,
            RoomDatabase::class.java,
            "app-db"
        ).build()
    }
}

Never pass an Activity Context to a singleton, service, or long-lived object.

Lifecycle-Aware Components

Jetpack's lifecycle components eliminate many leak patterns automatically:

class MyActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Collects only when Activity is in STARTED state
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    updateUI(state)
                }
            }
        }
    }
}

repeatOnLifecycle cancels collection when the Activity is stopped and resumes when it starts again. This prevents the coroutine from holding a reference to the destroyed Activity.

WeakReference Usage

When you must hold a reference to an Activity or Fragment from a long-lived object:

object EventManager {
    private val listeners = mutableListOf<WeakReference<EventListener>>()
    
    fun register(listener: EventListener) {
        listeners.add(WeakReference(listener))
    }
    
    fun notify(event: Event) {
        listeners.removeAll { it.get() == null }
        listeners.forEach { ref ->
            ref.get()?.onEvent(event)
        }
    }
}

WeakReferences allow the GC to collect the listener when the Activity is destroyed. Clean up null references periodically.

ViewModel as the Bridge

ViewModels survive configuration changes and provide a safe place to hold data:

class MyViewModel : ViewModel() {
    private val _items = MutableStateFlow<List<Item>>(emptyList())
    val items: StateFlow<List<Item>> = _items.asStateFlow()
    
    fun loadItems() {
        viewModelScope.launch {
            _items.value = repository.getItems()
        }
    }
}

The ViewModel never holds a reference to the Activity or Fragment. It only exposes data flows that the UI observes. This is the standard architecture for leak-free Android apps.

Checklist Before Shipping

  1. No static references to Activity, Fragment, or View
  2. All register/unregister calls are paired
  3. Handlers use lifecycle-aware wrappers
  4. Coroutines use repeatOnLifecycle or flowWithLifecycle
  5. WebView is destroyed in onDestroy()
  6. Singletons use Application Context
  7. Anonymous inner classes don't capture Activity references
  8. Run LeakCanary in debug builds for the entire development cycle

Quiz

1. What happens when an Activity is destroyed but a strong reference to it still exists somewhere in the app?

Question 1 options

2. You rotate the screen and LeakCanary reports that the old MainActivity instance is leaking. The leak trace shows a Handler.postDelayed callback. What is the most likely cause?

Question 2 options

3. Which Context should a singleton use to initialize a Room database to avoid leaking an Activity?

Question 3 options

4. What does repeatOnLifecycle do in a coroutine scope?

Question 4 options

Flashcards

Question

What is a memory leak in Android?

Answer

A memory leak occurs when an object cannot be garbage collected because a long-lived object still holds a strong reference to it, even after the object is no longer needed.

Question

What is the most common cause of Activity leaks in Android?

Answer

Holding a strong reference to an Activity from a static field, singleton, or inner class that outlives the Activity lifecycle.

Question

How do you read a LeakCanary leak trace?

Answer

Find the object marked Leaking: YES, then follow the reference chain upward to find the strong reference preventing garbage collection. The first Leaking: NO object above it is the root cause.

Question

Why should you use applicationContext instead of an Activity reference in a singleton?

Answer

applicationContext lives as long as the app process. An Activity reference in a singleton would prevent the Activity from being garbage collected after it is destroyed, causing a memory leak.

Revision Notes

Key Takeaways

  • 1. Memory leaks occur when strong references prevent GC from reclaiming destroyed objects
  • 2. LeakCanary is the primary tool for detecting leaks in debug builds
  • 3. The reference chain in a leak trace shows exactly why an object cannot be collected
  • 4. Use lifecycle-aware components like repeatOnLifecycle to prevent coroutine leaks
  • 5. Singletons must use applicationContext, never Activity references

Interview Tips

  • Be ready to explain what a memory leak is and why it matters on mobile devices
  • Walk through how you would diagnose a leak using LeakCanary's reference chain
  • Discuss the difference between Activity Context and Application Context
  • Explain how lifecycle-aware components prevent leaks in modern Android architecture
  • Know the common leak patterns: static references, inner classes, Handlers, unregistered listeners

Cheat Sheet

Memory Leaks Cheat Sheet

What is a leak: Object cannot be GC because a strong reference still exists.

Common causes:

  • Static references to Activity/Context
  • Inner classes holding outer Activity reference
  • Handler.postDelayed without lifecycle awareness
  • Unregistered listeners/receivers
  • WebViews not destroyed
  • Coroutines outliving Activity

Detection tools:

  • LeakCanary (debug builds)
  • Android Studio Memory Profiler
  • Firebase Crashlytics (production OOM crashes)

Fixes:

  • Use applicationContext in singletons
  • Use WeakReference for optional long-lived callbacks
  • Use repeatOnLifecycle for coroutine collection
  • Pair register/unregister calls
  • Destroy WebViews in onDestroy()

Architecture:

  • ViewModel holds data, never Activity references
  • Use StateFlow/LiveData for UI communication
  • Use lifecycle-aware components throughout