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
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.
Five types of leaks cover 95% of cases in mobile development. Each has its own cause and characteristic code pattern.
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>.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
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.
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.
// automatic unsubscription via Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// coroutines with lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
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.
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.
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.
Four tools cover leak detection from automatic detection to deep Heap Dump analysis.
| Tool | Method | Output Format |
|---|---|---|
| LeakCanary | Automatic monitoring | Heap Dump + leak stack trace |
| Android Memory Profiler | Manual monitoring | Memory graph + Heap Dump |
| MAT (Eclipse) | Deep analysis | Dominator Tree report + GC Root path |
| Perfetto | System-wide tracing | Timeline + 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.
Prevention of leaks is built into the development process through a set of rules and tools that check code at every stage.
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.
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.
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.
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.
// 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
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.
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.
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.
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.
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
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