๐Ÿงช Testing, Debugging, and Performance

Android Memory Leaks: Causes and Solutions

A memory leak keeps objects alive that should be garbage collected, often entire Activities with their Views and bitmaps. Leaks cause slowdowns, jank and eventually OutOfMemoryError crashes. Here's how leaks happen in Android apps, how to find them, and how to prevent them.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. How a leak happens
  2. Common causes and fixes
  3. The Fragment binding leak
  4. Finding leaks with LeakCanary
  5. Investigating with Android Studio
  6. Memory problems that aren't leaks
  7. Real investigations
  8. Prevention habits
  9. Frequently asked questions
  10. Conclusion

Garbage collection frees objects nothing references any more. A leak happens when something long-lived (a singleton, a static field, a running coroutine, a registered listener) keeps a reference to something short-lived, like an Activity that has already been destroyed. Every rotation can then add another dead Activity to memory.

How a leak happens

A singleton holding a destroyed Activity A long-lived singleton, such as an analytics manager, holds a listener that references Activity A. After rotation, Activity A is destroyed and Activity B is created, but A cannot be garbage collected because the singleton still references it, along with its whole view hierarchy and bitmaps. Singleton(lives forever) listener ref Activity A (destroyed)+ View hierarchy+ bitmapscan't be collected Activity B(after rotation)
The leak isn't the listener itself. It's the reference path from a long-lived root to a dead object.

Common causes and fixes

CauseExampleFix
Activity Context in a singletonImageLoader.init(activity)Use applicationContext; don't store UI contexts
Unregistered listeners/callbacksLocation, sensor or broadcast listenersUnregister in the matching lifecycle callback or DisposableEffect
Fragment View referencesBinding kept after onDestroyViewNull out the binding in onDestroyView
Long-running work capturing UIGlobalScope.launch { textView.text = โ€ฆ }Lifecycle-aware scopes; never capture Views in background work
Static or companion fields holding Viewscompanion object { var lastView: View? }Don't. Pass IDs or state instead
Inner classes and anonymous handlersA delayed Handler message referencing the ActivityRemove callbacks on destroy; prefer coroutines with lifecycle scopes
ViewModel holding a View or Activityclass VM(val activity: Activity)ViewModels must not reference UI objects

The Fragment binding leak

Correct View Binding in FragmentsKotlin
class ProfileFragment : Fragment(R.layout.fragment_profile) {
    private var _binding: FragmentProfileBinding? = null
    private val binding get() = _binding!!      // only valid between onViewCreated and onDestroyView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        _binding = FragmentProfileBinding.bind(view)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null                           // the Fragment may outlive its View (back stack)
    }
}

Finding leaks with LeakCanary

app/build.gradle.ktsGradle KTS
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:<version>")   // debug builds only
}

LeakCanary watches destroyed Activities, Fragments, Views and ViewModels. When one isn't collected, it dumps the heap, analyses it, and shows the leak trace: the reference chain from a GC root to the leaked object. Read it bottom-up to find the reference that shouldn't exist.

Investigating with Android Studio

  1. Open the Profiler and choose memory tracking for your app.
  2. Reproduce the suspected leak, for example by rotating several times or opening and closing a screen.
  3. Force garbage collection, then capture a heap dump.
  4. Filter by your Activity class. More instances than expected indicate a leak, and Android Studio can highlight leaked Activities and Fragments.
  5. Inspect the references of a leaked instance to find the GC root path.

Memory problems that aren't leaks

  • Large bitmaps: decoding full-resolution images for thumbnails. Use an image library that downsamples.
  • Unbounded caches: in-memory lists that grow forever. Use LruCache with limits.
  • Holding large data in UI state: page it with Paging, or keep it in the database.

Real investigations

OOM after many rotations

Leak trace: an analytics singleton held the Activity via a listener. Fix: application context and unregistering.

Fragment back stack growth

Bindings kept after onDestroyView. Fix: null out the binding, or move the screen to Compose.

Slow app after long use

Not a leak: an image cache without limits. Fix: size the cache and respond to onTrimMemory.

Prevention habits

  • Use lifecycle-aware components and scopes (viewModelScope, repeatOnLifecycle, DisposableEffect).
  • Inject the application context with Hilt's @ApplicationContext into long-lived objects.
  • Keep LeakCanary in debug builds and treat its reports as bugs.
  • Compose reduces many View-related leaks, but listeners and contexts can still leak.

Frequently asked questions

Should I ship LeakCanary in release builds?

No. Add it with debugImplementation only. Heap dumps are expensive and may contain user data.

Does Kotlin or Compose eliminate leaks?

They reduce common causes, but any long-lived reference to a short-lived object still leaks.

Is a ViewModel holding Application context a leak?

No. The Application lives as long as the process. AndroidViewModel exists for this, though injecting specific dependencies is cleaner.

Conclusion

Memory leaks are reference paths from long-lived roots to dead objects, usually Activities, Fragments' Views or Contexts. Detect them with LeakCanary and heap dumps, fix the offending reference, and prevent them with lifecycle-aware scopes and application context.

Next step: tackle the other major stability issue in ANR Errors Explained.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Performance work starts with understanding how Android apps are built and how they behave at runtime. Strengthen those fundamentals and the profiling tools start to make sense.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp