Skip to content
All posts
July 11, 20265 min read

Debugging Memory Leaks in Android With LeakCanary

Memory leaks rarely crash on the first screen — they crash users three navigations deep. Here's the LeakCanary workflow I use to catch the common Android leak sources before they hit production.

AndroidMemory LeaksLeakCanaryDebuggingPerformance
Share:

A memory leak almost never announces itself. The app runs fine, then a user who keeps it open for an hour and bounces between screens hits an

code
OutOfMemoryError
you can't reproduce in a five-minute test. LeakCanary is the tool that turns that invisible slow bleed into a concrete stack trace pointing at the exact reference holding memory hostage.

Setup Takes One Line

LeakCanary installs itself; there's no init code in

code
Application
.

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

It only runs in debug builds. After an

code
Activity
or
code
Fragment
is destroyed, LeakCanary checks whether it was actually garbage collected. If it wasn't, you get a notification with the reference chain.

Read the Leak Trace From the Bottom Up

The trace shows what's keeping your object alive. The bottom is the GC root; the top is the leaked object. The line that matters is the reference that shouldn't exist — usually something static or long-lived holding a short-lived object.

text
┬───
│ GC Root: Global variable in native code
│
├─ MySingleton.context  ← holds an Activity. This is the leak.
│
╰→ MainActivity instance (leaking)

When you see an

code
Activity
retained by a singleton, you've found it.

The Leaks I See Most Often

A Context held by a singleton. Passing an

code
Activity
to something long-lived pins the whole view hierarchy. Use the application context for anything that outlives a screen.

kotlin
// Leak: Activity context stored in a singleton
class Analytics(private val context: Context)
Analytics(activity) // pins the Activity forever

// Fix: use application context
Analytics(activity.applicationContext)

A coroutine outliving its screen. A job launched in the wrong scope keeps its captured references alive. Tie work to

code
viewModelScope
or a lifecycle-aware scope so it cancels automatically.

kotlin
class MyViewModel : ViewModel() {
    fun load() = viewModelScope.launch { /* cancelled with the ViewModel */ }
}

A listener you registered but never removed. Register in

code
onStart
, unregister in
code
onStop
. An unbalanced callback registration is a classic retention.

An inner-class handler. A non-static

code
Handler
with a delayed message holds its outer
code
Activity
until the message fires. Use a flow, a lifecycle-aware coroutine, or remove callbacks in
code
onDestroy
.

Verify With the Profiler

LeakCanary tells you what leaked. When I want to see how much memory is growing over time, I open Android Studio's Memory Profiler, repeat the suspect navigation a dozen times, and force a GC. If the count of an

code
Activity
climbs and never drops, the leak is confirmed and I can watch the fix flatten the line.

Make It Part of the Loop

The reason leaks reach production is that nobody navigates back and forth enough during normal testing. I treat any LeakCanary notification during development as a build-breaker, not a "later" item. Fixing one when the reference chain is fresh in front of you takes minutes; chasing an OOM report from the field takes a day.

Why Leaks Hide So Well

The reason memory leaks survive testing is that they don't violate any obvious contract. The screen renders, the feature works, the tests pass. Nothing is wrong in the sense a crash is wrong — there's just an object that should have been collected and wasn't, quietly holding a few megabytes. One leaked Activity is harmless. The problem is that leaks accumulate: navigate into the leaking screen fifty times over a long session and you've pinned fifty view hierarchies, and now the garbage collector is thrashing and the next allocation throws. The symptom appears far from the cause, both in time and in screens, which is exactly why it's so hard to catch by hand.

This is why I value tools that make the invisible visible. A leak you can't see is a leak you'll argue doesn't exist until a user's crash report proves otherwise. LeakCanary's whole value is converting "the app feels slow after a while" — the vaguest bug report there is — into a precise reference chain you can act on in minutes. It shifts the work from reproducing a mystery to reading a stack trace, and reading a stack trace is something every Android developer already knows how to do.

The mindset shift that helped me most was treating retained memory as a first-class correctness property, not a performance nicety to look at someday. A destroyed Activity that stays in memory is a bug in the same category as a null pointer; it's just one that fails later and quieter. Once I started treating leak notifications as build-breakers instead of suggestions, the OOM reports from the field essentially stopped. The cost of fixing a leak while its reference chain is fresh on your screen is a few minutes. The cost of diagnosing one from an aggregated crash dashboard weeks later, with no reproduction steps, is most of a day. Catching it early isn't just cleaner — it's the cheaper path by a wide margin.

Key Takeaways

  • Add LeakCanary as a
    code
    debugImplementation
    — it self-installs and watches destroyed Activities and Fragments automatically.
  • Read the leak trace bottom-up; the culprit is the long-lived reference that shouldn't be holding a short-lived object.
  • Use the application context, never an Activity context, for anything that outlives a screen.
  • Scope coroutines to
    code
    viewModelScope
    or a lifecycle scope so they cancel instead of retaining their captures.
  • Balance every listener registration with an unregister in the matching lifecycle callback.
  • Confirm fixes with the Memory Profiler by repeating the navigation and watching instance counts return to zero.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus