Memory leak — a situation where an application does not release memory occupied by objects that are no longer needed. In mobile development, this is especially critical: limited heap and the absence of swap lead to OutOfMemoryError and app crashes. According to Purdue University (2022), 35% of Android apps on Google Play contain at least one memory leak. Let's examine typical scenarios, diagnostic tools, and elimination methods.
Key Takeaways
Memory leak is a situation where allocated memory is not returned to the system after the object is no longer needed by the program. The garbage collector considers such an object alive because there is an active reference chain from a GC Root pointing to it.
In Java/Kotlin, the garbage collector works automatically, but it cannot determine that an object is logically unnecessary if there is a technical reference to it. The developer must explicitly break unnecessary connections. In Swift/Objective-C, ARC automatically counts references, but retain cycles block the counter from reaching zero.
The main danger of leaks is the cumulative effect. Each leak consumes a small amount of memory, but with repeated screen transitions (screen rotations, opening/closing Activities), leaks accumulate until the heap limit is exhausted.
Leak — the object is inaccessible to code but not removed by GC. Bloat — the object is logically needed but is stored in excessive quantity. Example of bloat: an image cache of 100 MB with a working set of 30 MB. Both problems lead to OOM, but the causes and treatment methods differ.
ART (Android Runtime) uses generational garbage collection with concurrent compaction. Memory is divided into Young generation, Old generation, and Large objects. Objects that survive several GC cycles are moved to Old generation, where collection happens less frequently — this speeds up regular cycles.
GC starts when the heap reaches a certain occupancy threshold (usually 75-85%). During GC, all application threads are paused (STW — Stop The World). The more live objects, the longer the pause. Leaks increase the number of live objects, lengthening GC pauses.
The collector determines live objects by traversing the graph from GC Roots: static fields, stack variables of active threads, JNI references. Any object reachable via references from these roots is considered alive — even if the developer knows it is no longer needed.
// Example: static collection as GC Root — permanent leak
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference does not prevent GC — correct behavior
}
}
WeakReference solves the problem: GC ignores weak references when determining live objects. If only weak references remain to an object, it will be collected in the nearest GC cycle.
Activity Context — the most widespread leak scenario in Android. If a singleton, static field, or long-lived service holds a reference to an Activity Context, the entire Activity with all its Views cannot be collected by GC. Solution: use Application Context for long-lived objects.
Handler and Posted Messages — Handler.postDelayed(runnable, delay) places a message in the Main Looper queue. If the Activity is destroyed before the delay expires, the message is still in the queue and holds a reference via Runnable → anonymous class → outer class (Activity).
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // mandatory: clear queue
super.onPause()
}
}
Inner Classes — a non-static inner class has an implicit reference to the outer class instance. If the outer class is an Activity and the inner class is passed somewhere externally (e.g., to RecyclerView.Adapter), the Activity cannot be collected.
Android Studio Memory Profiler — a built-in tool for real-time heap monitoring. Shows a memory usage graph, allocation count, and object types. Allows recording a heap dump and exporting it in HPROF format for analysis in MAT.
Eclipse MAT (Memory Analyzer Tool) — a desktop heap dump analyzer. Automatically builds Leak Suspects Reports, highlighting objects with the largest retained size and suggesting the suspected GC root chain for each suspicious object.
Xcode Memory Graph Debugger — for iOS. Pauses the application and visualizes the object graph. Retain cycles are highlighted in red; you can click any object to see its retain count and references.
| Tool | Capabilities | Complexity |
|---|---|---|
| Memory Profiler | Real-time graph, heap dump, Object Allocation Tracking | Low |
| Eclipse MAT | Dominator tree, Leak Suspects, OQL queries | Medium |
| LeakCanary | Automatic detection, leak trace in notification | Minimal |
| Xcode Memory Graph | Visual retain cycle graph, live object list | Low |
According to the Uber Engineering Blog, integrating automatic memory profiling (LeakCanary + heap dump analysis) into the CI/CD pipeline reduces memory-related production incidents by 60% within 3 months.
Replace Context — if an object outlives the Activity, use applicationContext. All long-lived objects (singletons, repositories, database helpers) should receive Application Context, not Activity Context. Exception: UI components that need access to theme or resources specific to the Activity.
Lifecycle-aware Components — using LifecycleObserver, DefaultLifecycleObserver, or reactive extensions automatically cancels subscriptions on onDestroy. Android Jetpack provides lifecycleScope and viewModelScope, which are cleaned up by the corresponding lifecycle event.
Static Inner Class — if the inner class does not need access to the outer class fields, make it static. A static inner class has no implicit reference to the outer class. If access is needed, use WeakReference for the explicit reference.
class MyActivity : AppCompatActivity() {
// ❌ Non-static inner class — implicit reference to MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Static inner class — no implicit reference
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
In iOS, use capture lists: [weak self] in closures that may outlive their creator. For delegates, use weak references (weak var delegate). For closures that are guaranteed to be called only during the lifetime of self, [unowned self] can be used, but with caution — accessing a deallocated object will cause a crash.
Frequently Asked Questions
In Android, perform several screen transitions (Activity A → B → A → B) and check adb shell dumpsys meminfo package_name. If Total PSS steadily grows and does not return to the original value — there is a leak. In iOS, similarly: use Debug Memory Graph in Xcode for visual inspection.
Yes, if the CoroutineScope is not canceled when the component is destroyed. A coroutine launched in GlobalScope continues executing even after Activity.finish(). Solution: use viewModelScope (canceled in onCleared) or lifecycleScope (canceled in onDestroy). For custom scopes, create lifecycle-aware scopes via LifecycleOwner.
Bitmap stores pixel data in native heap, not Java heap. This means Java GC does not see the actual size of the Bitmap. If Bitmap is not recycled via recycle() or the reference is not nulled, native memory is not freed. Use BitmapFactory with inSampleSize to load reduced copies and Glide/Coil for automatic cache management.
A static field is a GC Root. It lives as long as the class is loaded (in Android — as long as the Process is alive). If a static field references an Activity, Bitmap, View, or any other heavy object, that object will never be collected by GC. A static field is an eternal reference. Solution: store only WeakReference or nullify the static field in onDestroy.
ARC automatically releases objects when the strong reference count drops to zero. A retain cycle is the only way to leak under ARC. Always use weak for parent→child references where the child may outlive the parent (delegates, data sources). For closures, use the capture list [weak self] and check self for nil inside the closure.
Summary
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.
Read also