Memory Leak in Mobile Applications — What It Is, Causes, and Detection Methods

Author: IT Sectr Published: 2026-03-29 Reading time: 9 min

A memory leak is a situation where an application holds references to objects that are no longer needed, preventing the garbage collector from freeing occupied memory. According to LeakCanary, even well-written applications have 3–5 leaks per 10,000 lines of code. Each leak gradually reduces available memory, leading to slowdowns and OutOfMemoryError.

Key Takeaways

  • Memory Leak — an object remains in memory even though there are no active references to it from the application logic
  • Static references to Activity or Context — the most common cause of leaks in Android
  • LeakCanary — the standard tool for automatic leak detection in Android
  • WeakReference and Application Context — basic techniques for preventing leaks
  • Lifecycle-aware components eliminate a whole class of leaks related to subscriptions

What Is a Memory Leak

A memory leak is a situation where an object remains reachable through a chain of strong references, even though it is no longer logically needed by the application. The garbage collector (GC) considers such an object alive and does not free the memory it occupies. As a result, the available heap memory constantly decreases, and the frequency of GC pauses increases.

Unlike languages with manual memory management (C, C++), in Java/Kotlin a leak is not a forgotten free() but a forgotten reference. As long as a strong reference exists from a GC Root to the leaked object, the GC considers it necessary. Typical GC Roots: static fields, active threads, call stack, JNI global references.

The danger of leaks is their cumulative effect. One 100 KB leak is unnoticeable, but 100 such leaks take up 10 MB, and the application starts to lag due to frequent GC. A critical mass of leaks leads to OutOfMemoryError and application crashes. Symptoms of a leak: constant growth of memory consumption on the Profiler graph, frequent GC pauses with STW (Stop The World), and UI performance degradation.

Common Types of Leaks in Mobile Applications

Five types of leaks cover 95% of cases in mobile development. Each has its own cause and characteristic code pattern.

Static Reference to Activity or Context

The most well-known leak in Android is storing a static reference to an Activity or Context. Typical code: a static Activity field that is not nullified on onDestroy(). As long as the static field lives, the entire Activity lives with its View tree, which can occupy 1–10 MB. This is the classic leak that LeakCanary finds first.

Solution: never store Activity or Context in static fields. Use Application Context for singletons that outlive the Activity. If you need a reference to an Activity, use WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Inner Classes with Implicit References

Anonymous classes and non-static inner classes implicitly hold a reference to the containing class. A Runnable passed to a Handler that executes after onDestroy() holds the entire Activity. A Retrofit callback that closes over an Activity does the same. This is the most insidious type of leak — the implicit reference is not visible in the code.

Kotlin object expressions and lambdas also capture references to the outer class. Make inner classes static (or top-level in Kotlin) and pass outer references through WeakReference. For lambdas, use a Lifecycle-aware approach with viewLifecycleOwner.

Unsubscribed Listeners and Subscriptions

Subscribing to system services without unsubscribing is a direct leak. SensorManager, LocationManager, NotificationListener registered in onResume() without calling unregister in onPause() hold the Activity. Similarly: RxJava Disposable not added to CompositeDisposable, and a coroutine launched through GlobalScope.

Use Lifecycle-aware components: observe() with LifecycleOwner automatically unsubscribes on onDestroy(). For RxJava — viewLifecycleOwner.lifecycle.addObserver with DisposableObserver. For coroutines — lifecycleScope.launch() is tied to the lifecycle.

kotlin
// automatic unsubscription via Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// coroutines with lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap Without Recycle

A Bitmap takes up a significant amount of heap memory: one FullHD bitmap is 1920 × 1080 × 4 bytes = 8.3 MB. If a Bitmap is created for each list item and recycle() is not called when hiding, memory is quickly exhausted. On older Android versions (before 3.0), Bitmap was stored in native memory, but on modern versions it is in the Dalvik/ART heap, and the GC can only free it if there is no strong reference.

Use Glide or Coil for loading images — these libraries manage caching and recycling automatically. If working with Bitmap directly, call bitmap.recycle() for large images that are no longer displayed, and use inSampleSize to load reduced copies.

Fragment Reference After onDestroyView

A Fragment has two lifecycles: the Fragment itself and its View. After onDestroyView(), the View tree is destroyed, but the Fragment itself can remain in memory if there is an external reference. A typical mistake is storing a reference to a Fragment in a ViewPager adapter or in a navigation graph that is not cleared upon destruction.

Never store a reference to a Fragment in the fields of long-lived objects. Use childFragmentManager for nested fragments and observe() with LifecycleOwner for data transfer between them. ViewPager2 solved this problem at the API level: FragmentTransactionAdapter correctly manages the lifecycle.

How to Detect a Memory Leak

Detection of a leak requires verifying two facts: memory does not return after the expected lifetime and the number of objects of a certain type grows without decreasing. The diagnostic process includes three stages.

The first stage is a visual check through the Memory Profiler in Android Studio. Open the Memory tab, perform the target action (open and close the screen), press GC (Garbage Collection), and see if the memory returns to the initial level. If after 3–4 open-close cycles the memory consistently grows — there is a leak.

The second stage is taking a Heap Dump. In the Memory Profiler, press Dump Java Heap. Open the resulting .hprof file in Android Studio: you will see all objects in the heap with sizes and references. Look for classes whose count should be zero after closing the screen. For example, MainActivity with a count of 2 after closing is an obvious leak.

The third stage is analyzing Retained Size and GC Root. In Android Studio, analyze Retained Size: how much memory will be freed if you remove this object. The path from GC Root to the object shows what holds it: Static field → HashMap → Activity — and you see the leak point. The Reference widget panel shows all holders of the object.

Tools for Leak Detection

Four tools cover leak detection from automatic detection to deep Heap Dump analysis.

ToolMethodOutput Format
LeakCanaryAutomatic monitoringHeap Dump + leak stack trace
Android Memory ProfilerManual monitoringMemory graph + Heap Dump
MAT (Eclipse)Deep analysisDominator Tree report + GC Root path
PerfettoSystem-wide tracingTimeline + native memory

LeakCanary is a must-have for any Android project. It automatically detects leaks at the end of the Activity/Fragment lifecycle and shows the exact leak location with a stack trace. Integration: one line in build.gradle. LeakCanary 2.x does not require manual initialization — it automatically registers the Application Watcher.

How to Prevent Memory Leaks

Prevention of leaks is built into the development process through a set of rules and tools that check code at every stage.

Strong Reference Rule

Never store a reference to an Activity, Fragment, or View in a static field, singleton, or long-lived object. If a reference is unavoidable, use WeakReference or store data through ViewModel, which lives exactly as long as needed and does not hold a View directly.

Lifecycle-Aware Architecture

ViewModel and LiveData from Android Architecture Components solve the lifecycle problem at the architecture level. ViewModel survives screen rotation and does not contain View references. LiveData automatically unsubscribes the observer on onDestroy(). Use them instead of manual subscription to system services.

Code Review Focused on GC Root

During code review, pay attention to: static fields with Context/View types, anonymous classes, lambdas closing over Activity, manual subscriptions, RxJava disposable without composite, storing Fragment through Bundle. In Kotlin, additionally check coroutines for launch without lifecycle binding.

Automatic Check in CI

LeakCanary can work as part of the test pipeline: run acceptance tests with LeakCanary and fail the build if a leak is found. This prevents leaks from reaching production. Supplement the check with Android Lint’s StaticFieldLeak rule — it finds potential leaks at the static analysis level.

kotlin
// LeakCanary in tests
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fail if there is a leak
    }
}

Frequently Asked Questions

How is a memory leak different from OutOfMemoryError?

A leak is the cause, and OutOfMemoryError is the consequence. One leak does not lead to OOM, but the accumulation of dozens of leaks exhausts the Heap. OOM is a fatal exception, while a leak is a pattern that leads to it over time.

How to find a leak without LeakCanary?

Through Android Memory Profiler: open and close the screen 5 times, invoke GC after each close. If the memory does not return to the baseline level — there is a leak. Take a Heap Dump and find the Activity class whose count is greater than 0 after closing.

Can Kotlin prevent leaks at the language level?

Partially. Kotlin solves the null-safety problem but does not manage strong references. Coroutines with lifecycleScope and viewModelScope prevent leaks from background tasks, while sealed class and data class reduce the number of states that lead to leaks. The main protection is architectural patterns, not language features.

Why does LeakCanary find a leak that isn’t there?

LeakCanary sometimes gives false positives: an object may be temporarily held by the system (for example, InputMethodManager holds the last View). Check manually: if Retained Size < 1 KB and the GC Root is a system service, it is likely a false positive.

Do memory leaks only exist on Android?

No. Leaks are possible on any platform with a GC: iOS (Swift/Objective-C), Flutter (Dart), web browsers (JavaScript). The mechanisms are the same — strong reference from GC Root. On iOS, ARC automatically manages memory, but retain cycles between objects create the same leak.

Summary

  • Memory Leak — an object that the GC cannot free due to a forgotten strong reference
  • Static references to Activity and Context — the most common cause of leaks
  • Implicit references through anonymous classes, lambdas, and RxJava subscriptions are more insidious than explicit ones
  • LeakCanary automatically finds leaks and shows the exact stack trace
  • Lifecycle-aware components (ViewModel, LiveData, lifecycleScope) eliminate a class of leaks
  • Heap Dump and Retained Size analysis — the main method of manual diagnostics
  • Prevention includes code review focused on strong references and CI checking with LeakCanary

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also