0Pricing
Android Academy · Lesson

Memory Leaks and Fixes

Find and stop leaks.

Memory Leaks and Fixes is a free Android Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is a Memory Leak?

A memory leak happens when objects that are no longer needed cannot be garbage collected because something still holds a reference to them. On Android the classic victim is an Activity or Context that lingers after its screen is destroyed.

Leaks grow your heap over time, trigger more frequent garbage collection (which pauses the UI and causes jank), and eventually crash the app with an OutOfMemoryError. This lesson shows how to find and fix them.

How GC Decides What to Keep

Android's garbage collector keeps any object that is reachable from a GC root (live threads, static fields, etc.). Anything unreachable is freed.

A leak is simply an unwanted reachability path: a long-lived object holding a reference to a short-lived one. The pure-Kotlin demo below shows reachability keeping an object alive.

object GlobalCache {
    val items = mutableListOf<ByteArray>()
}

fun cacheSomething() {
    // This 1MB array stays alive forever because GlobalCache
    // (a static singleton, a GC root) keeps referencing it.
    GlobalCache.items.add(ByteArray(1_000_000))
}

fun main() {
    repeat(3) { cacheSomething() }
    println("Held arrays: ${GlobalCache.items.size}") // never freed
}

The Classic: Leaking an Activity

The most common Android leak is a static or long-lived object holding a Context. When the Activity is destroyed (rotation, navigation) the system wants to free it, but the static reference keeps it alive forever.

The code below leaks: a singleton caches an Activity context.

// LEAK: static field holds an Activity Context
object Analytics {
    var context: Context? = null   // <- keeps Activity alive
    fun init(ctx: Context) { context = ctx }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Analytics.init(this) // passing the Activity -> leak on rotation
    }
}

Fix: Use the Application Context

When you must store a Context in something long-lived, store the application context, which lives for the whole process and is safe to retain.

Only keep an Activity/View context for as long as that screen exists. This single rule prevents most Android leaks.

object Analytics {
    private var appContext: Context? = null
    fun init(ctx: Context) {
        // applicationContext is process-scoped and safe to keep
        appContext = ctx.applicationContext
    }
}

// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the Activity

Inner Classes and Handlers

A non-static inner class (including most listeners, Runnables, and Handler callbacks) holds an implicit reference to its outer class. If that work outlives the screen, it leaks the screen.

A delayed Handler.postDelayed is a frequent culprit: the pending message keeps the Activity alive until it fires.

// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)

// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
    super.onDestroy()
    handler.removeCallbacksAndMessages(null)
}

Coroutines: Scope Your Work

A coroutine that outlives its screen leaks everything it captures. The fix is structured concurrency: launch work in a scope tied to a lifecycle so it is cancelled automatically.

In a ViewModel use viewModelScope; in a UI controller use lifecycleScope. When the owner is destroyed, the scope cancels and references are released.

class FeedViewModel : ViewModel() {
    fun load() {
        // Cancelled automatically when the ViewModel is cleared
        viewModelScope.launch {
            val feed = repository.fetchFeed()
            _state.value = feed
        }
    }
}

// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()

DisposableEffect in Compose

In Compose, anything you register must be unregistered when the composable leaves. DisposableEffect gives you an onDispose block for exactly that: listeners, observers, sensors, broadcast receivers.

Forgetting cleanup here leaks the callback and everything it captures.

@Composable
fun LocationDisplay(manager: LocationManager) {
    DisposableEffect(manager) {
        val listener = LocationListener { /* update */ }
        manager.register(listener)
        onDispose { manager.unregister(listener) } // prevents the leak
    }
}

Finding Leaks with LeakCanary

LeakCanary is the standard tool for catching leaks automatically in debug builds. Add the dependency and it watches destroyed objects; if one is not garbage collected, it dumps the heap and shows you the exact reference chain keeping it alive.

That chain is the fix: it points straight at the offending field or listener.

// build.gradle.kts (app module)
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
//   Analytics.context -> MainActivity (leaked)

Heap Dumps in the Profiler

For leaks LeakCanary does not flag (slow growth, native, large caches), use the Memory Profiler. Capture a heap dump, force GC, and inspect which classes have surprisingly many instances.

A growing instance count of your Activity, Fragment, or ViewModel after navigating away is a smoking gun. The profiler also shows the path to GC roots so you can trace the reference.

Bitmaps and Large Allocations

Not every memory problem is a reference leak. Large bitmaps and unbounded caches can blow the heap on their own. A full-resolution photo can be tens of megabytes in memory.

Let an image library (Coil/Glide) downsample to the displayed size, and bound any cache you create with an explicit max. The snippet shows a size-bounded LRU cache.

// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
    override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.

Leak-Prevention Checklist

Build these habits and most leaks never happen:

  • Store applicationContext in long-lived objects, never an Activity/View.
  • Run async work in a lifecycle-scoped coroutine (viewModelScope, lifecycleScope).
  • Always unregister listeners/receivers (onDispose, onDestroy).
  • Cancel delayed Handlers and timers.
  • Bound caches and downsample bitmaps.
  • Keep LeakCanary in debug builds and act on its traces.

Quick Check

You need a singleton object to keep a Context for its whole lifetime. Which context should it hold to avoid leaking a screen?

Recap: Keep Memory Clean

You learned what leaks are and how to stop them:

  • Leaks are unwanted reachability paths keeping dead objects alive.
  • The classic leak is a static/long-lived reference to an Activity/Context — store applicationContext instead.
  • Inner-class listeners, delayed Handlers, and unscoped coroutines leak their owner — scope and cancel them.
  • Use DisposableEffect in Compose to unregister callbacks.
  • LeakCanary and the Memory Profiler reveal the reference chain.
  • Bound caches and downsample bitmaps.

Next: making the app start fast with startup optimization and baseline profiles.

Frequently asked questions

Is the “Memory Leaks and Fixes” lesson free?

Yes — the full text of “Memory Leaks and Fixes” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “Memory Leaks and Fixes”?

Find and stop leaks. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Memory Leaks and Fixes” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Measuring Performance
  2. Taming Recomposition
  3. Memory Leaks and Fixes
  4. Startup and Baseline Profiles
← Back to Android Academy