Memory Leak: What It Is, Typical Scenarios, and Diagnostics

Author: IT Sectr Published: 2026-07-29 Reading time: 10 min

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

  • GC Root — entry point through which the garbage collector determines live objects
  • Context leak — passing Activity Context to a singleton holds the entire View hierarchy
  • Handler with postDelayed — if Activity is destroyed, Handler prevents it from being GC'd
  • Heap dump — primary method for leak analysis via MAT or Android Profiler
  • SoftReference — an alternative to WeakReference for caches that auto-clear when memory is low

What Is a Memory Leak in Mobile Applications?

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.

How Is a Leak Different from Bloat?

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.

How Does the Garbage Collector Work and Why Do Leaks Occur?

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.

kotlin
// 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.

Typical Leak Scenarios in Android and iOS

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).

kotlin
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.

  • TimerTask and ScheduledExecutorService — tasks scheduled before Activity destruction
  • BroadcastReceiver — unregistered in onPause/onDestroy continues to hold Context
  • ViewModel with View reference — ViewModel outlives Activity, reference to View leads to leak
  • Retrofit Call — if Call is not canceled, the response arrives at a destroyed Fragment

Memory Leak Diagnostic Tools

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.

ToolCapabilitiesComplexity
Memory ProfilerReal-time graph, heap dump, Object Allocation TrackingLow
Eclipse MATDominator tree, Leak Suspects, OQL queriesMedium
LeakCanaryAutomatic detection, leak trace in notificationMinimal
Xcode Memory GraphVisual retain cycle graph, live object listLow

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.

Methods for Eliminating Leaks

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.

kotlin
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

How to find a leak without special tools?

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.

Can a Kotlin coroutine cause a leak?

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.

How does Bitmap affect leaks?

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.

What is a leak through a static field?

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.

How to avoid leaks in iOS with ARC?

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

  • Memory leak — an object is inaccessible to code but is not removed by GC because there is an active reference from a GC Root
  • GC Roots include static fields, stack variables, and JNI references; any object reachable from them is alive
  • Context leak — the most widespread problem in Android: passing Activity Context to a singleton or static field
  • Handler and Inner Class — the second most common cause: uncanceled messages in the Looper queue hold a reference to the Activity
  • LeakCanary — the standard auto-detection tool; takes a heap dump and shows the exact GC root chain
  • lifecycleScope and viewModelScope solve the coroutine leak problem — automatic cancellation on destroy
  • Profile memory in CI/CD: LeakCanary in debug + heap dump analysis in test runs should block merging on new leaks

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