Android Memory Leaks: Root Causes and Diagnosis
Memory is the most constrained resource on a mobile device. In Android’s runtime environment (ART/Dalvik), Memory Leaks occur when an object is no longer functionally required by the application but remains reachable from a GC Root (such as an active thread, a static variable, or a JNI reference). This prevent the Garbage Collector (GC) from reclaiming the memory, eventually leading to OutOfMemoryError (OOM) and application instability.
1. Primary Sources of Leaks
I. Static Context References
A static variable (or a Kotlin object property) exists for the entire duration of the application process.
- The Error: Storing an
Activityinstance in a static variable. Because the static variable outlives the Activity, the Activity—and its entire associated View hierarchy—cannot be garbage collected even afteronDestroy(). - The fix: If a singleton requires a
Context, always usecontext.applicationContext. Its lifecycle matches the process, preventing leaks.
II. Inner Classes and Anonymous Handlers
Non-static inner classes hold an implicit reference to their outer class instance.
- The Error: Posting a delayed message via a
Handlerdefined as an inner class of an Activity. The message in theMessageQueueholds a reference to the Handler, and the Handler holds a reference to the Activity. If the message delay is 10 seconds, the Activity is leaked for 10 seconds even if the user leaves the screen. - The fix: Use Static Nested Classes and wrap the Activity reference in a
WeakReference. This allows the GC to reclaim the Activity whenever necessary.
III. Unregistered Callbacks
Registering with system services (e.g., LocationManager, SensorManager) or global event buses (e.g., EventBus) creates a strong reference to the listener.
- The fix: Adhere to the "Symmetry Rule": unregister every listener in the lifecycle counterpart where it was registered (e.g.,
onStart/onStop).
2. Reference Types: Fine-Grained GC Control
To handle complex lifecycles, Java/Kotlin provides four reference strengths:
| Type | GC Behavior | Best Use Case |
|---|---|---|
| Strong | Never collected while reachable. | 99% of variables. |
| Soft | Collected only during memory pressure. | Large image caches. |
| Weak | Collected during the next GC pass. | Handler-Activity bridges. |
| Phantom | Notification-only; never directly accessible. | Native resource cleanup. |
3. Tooling: Automated Detection with LeakCanary
LeakCanary is the industry-standard library for identifying leaks during the development phase.
- Mechanism: It hooks into the
onDestroy()callback of Activities and Fragments. It creates aWeakReferenceto the expiring object and manually triggers a GC pass. If the object remains in memory after a timeout, it performs a Heap Dump and analyzes the object graph to identify the "Leak Trace"—the chain of references from a GC Root to your leaked object.
4. Modern Prevention: Scoped Concurrency
The Jetpack library suite has introduced tools that make manual lifecycle management largely obsolete:
viewModelScope: Automatically cancels all background coroutines when the associatedViewModelis cleared.lifecycleScope: Automatically pauses or cancels tasks based on the Activity/Fragment lifecycle state.
Because these scopes automatically clean up background tasks, they prevent the "hanging thread" problem, which was historically the most common source of asynchronous memory leaks in Android.