Opening
Memory leaks. Every Android developer dreads them. You see the warnings in your profiler, maybe LeakCanary pops up with a scary notification, and your app gets slower and slower until the system kills it. But what is actually happening? Why do memory leaks occur in a language with automatic garbage collection? Are we not supposed to be free from manual memory management? The answer lies in how objects reference each other — the object graph. Today we are going to look at how the garbage collector actually decides what to keep and what to throw away, why Android apps are uniquely prone to leaks, and what is really going on when LeakCanary tells you something is wrong. Once you see the object graph, memory leaks stop being mysterious and start being obvious.
Objects Point to Other Objects
Let us build up the mental model from scratch. Every object in your app's memory contains two kinds of things: data — like numbers, booleans, bytes — and references to other objects. Your Activity holds a reference to its ViewModel. Your ViewModel holds a reference to a Repository. Your Repository holds a reference to a Retrofit service. Your Retrofit service holds a reference to an OkHttpClient. And on and on. These references form a web — a directed graph. Each object is a node, each reference is an arrow pointing from one node to another. This is the object graph. And it is the single most important structure the garbage collector cares about. Not your class hierarchy, not your inheritance tree. The GC cares about who points to whom. That is what determines whether an object lives or dies.
GC Roots: Where the Tracing Starts
The garbage collector does not check every object in memory and ask "are you still useful?" That would be impossibly slow. Instead, it starts from a specific set of starting points called GC roots. What are GC roots? They are references that the runtime considers inherently alive. Local variables on active thread stacks. Static fields — which in Kotlin means companion object properties and top-level properties. Active threads themselves. JNI references from native code. Think of GC roots like the main electrical panel in a building. If a wire connects to the panel, power flows through it to whatever is on the other end. The GC starts from every root and follows references outward, marking every object it can reach as "alive." When the tracing is done, anything not marked is unreachable — no code in your app can possibly access it — so it gets collected. Freed. Its memory is reclaimed.
The Rule: If ANY Path Reaches You, You Live
Here is the key rule, and it is absolute. If there is any path of references from any GC root to your object, that object stays alive. It does not matter if the path is five references long. It does not matter if the path goes through objects you forgot about. It does not matter if you think the object should be dead. If there is a path, it lives. Let me give you an example. Say you have a static HashMap — a companion object map in Kotlin. You put an entry in it with an Activity as the value. That Activity holds a reference to its entire view hierarchy — every button, every text view, every image. Even after the Activity is destroyed, even after onDestroy is called, if that HashMap entry still exists, the GC traces from the static field root, through the HashMap, to the Activity, to every view. None of it can be collected. All of it leaks. One forgotten reference in one map, and megabytes of memory are locked up forever.
Common Android Leaks
Let us walk through the most common memory leaks in Android, because they all follow the same pattern. First: an Activity reference captured in a callback or listener. You pass your Activity as a click listener or you register it as a callback with some manager, and that manager outlives the Activity. The manager holds a reference to the Activity, the Activity holds its views, and none of it can be collected. Second: a ViewModel holding a direct reference to a View or an Activity. ViewModels survive configuration changes — that is their whole purpose. But if a ViewModel holds a reference to a View from the old Activity, that old Activity and all its views leak. This is why ViewModels should never reference Views, Fragments, or Activities directly. Third: static references to Context. A companion object property that holds an Activity context. That Activity can never be garbage collected for the entire lifetime of the app. Use applicationContext when you need a long-lived Context. Fourth: inner classes and anonymous objects. A non-static inner class in Java — or a lambda that captures this in Kotlin — holds an implicit reference to the outer class. If that inner class outlives the outer Activity, you have a leak.
The Real Rule: Lifetime Mismatch
All of these leaks share a single root cause, and I want you to burn this rule into your brain. Memory leaks happen when a shorter-lived object is referenced by a longer-lived object. The Activity lives for one screen visit. The ViewModel lives across configuration changes. The Application lives for the entire process. A static field lives forever. When something long-lived holds a reference to something short-lived, the short-lived thing cannot die when it should. It is kept alive artificially. So the real rule for avoiding leaks is: always think about lifetimes. Before you store a reference, ask yourself: will the thing I am storing this in outlive the thing I am storing? If yes, you have a potential leak. Use a WeakReference, use a lifecycle-aware component, or restructure so the reference flows in the other direction — from short-lived to long-lived, which is always safe.
LeakCanary: What It Actually Does
LeakCanary is one of the most popular memory leak detection libraries in Android. But what does it actually do under the hood? Here is the process. When an Activity or Fragment is destroyed, LeakCanary wraps a reference to it in a WeakReference and registers it with a ReferenceQueue. If the garbage collector collects the object, the WeakReference shows up in the queue. LeakCanary waits a few seconds, triggers a GC, and then checks: did the reference show up in the queue? If yes — great, the object was collected, no leak. If no — the object was not collected, which means something is still holding a strong reference to it. At this point, LeakCanary dumps the heap — a snapshot of every object in memory and every reference between them. Then it analyzes this heap dump to find the shortest path from a GC root to the leaked object. That path is exactly what it shows you in the leak trace. It is literally showing you the chain of references that is keeping the dead object alive. Once you see that chain, the fix is usually obvious: break one of the links.
Reading a Leak Trace
Let us read a real leak trace so you know what to look for. A typical LeakCanary trace looks something like this. At the top, you see the GC root — maybe a static field or a thread. Then arrows pointing down through a chain of objects. Maybe a singleton service, holding a listener field, which is an anonymous class instance, which has a reference to an Activity. At the bottom is the leaked object — your Activity or Fragment. Each arrow in the trace is a reference. Each object is an intermediate holder. Your job is to find the link that should not exist and break it. Usually, it is the listener registration — you forgot to unregister in onStop or onDestroy. Sometimes it is a long-running coroutine that captured a reference to the Activity's scope. Sometimes it is a global cache that accumulated entries without evicting them. The trace tells you exactly where to look.
Weak References: The Escape Hatch
When you need a reference to an object but you do not want to prevent its garbage collection, you use a WeakReference. A WeakReference tells the GC: I am interested in this object, but I do not need it to stay alive for my sake. If no strong references remain, collect it. I will deal with it being gone. In code, you wrap the object: WeakReference(myActivity). When you want to access it later, you call get(). If it returns null, the object was collected. If it returns the object, it is still alive. WeakReferences are a critical tool for breaking leak patterns. If that callback manager held a WeakReference to the Activity instead of a strong reference, the Activity could be collected normally. The manager would get null when it tried to invoke the callback, and it could clean up gracefully. We will explore WeakReferences in much more depth in a later video. For now, just know that they exist and they let you observe an object without prolonging its life.
Visualizing the Object Graph
I want you to start thinking visually about your app's object graph. Picture it: your Application at the top, with strong references flowing down to singletons — your Retrofit instance, your Room database, your repositories. Below those, your ViewModels, each holding references to repositories. Below those, your Activities and Fragments, holding references to Views. The arrows should predominantly flow from long-lived to short-lived? No — that is the leak pattern. The safe direction is the opposite: short-lived objects reference long-lived objects. An Activity references the Application context — safe, because the Application outlives the Activity. A ViewModel references a Repository singleton — safe, because the singleton outlives the ViewModel. Problems occur when the arrows go upward — when a singleton references an Activity, when a Repository references a ViewModel. Always ask: which direction do my references flow?
Practical Prevention
Let me give you concrete prevention strategies. First: unregister everything. If you register a listener in onStart, unregister in onStop. If you register in onCreate, unregister in onDestroy. Be symmetrical. Second: never store Activity or View references in ViewModels. Pass data, not references. Use LiveData or StateFlow to communicate from ViewModel to UI. Third: avoid static references to Context. If you need a Context in a singleton, take applicationContext, never an Activity. Fourth: be careful with coroutines. A coroutine launched in viewModelScope is fine — it cancels when the ViewModel clears. A coroutine launched in GlobalScope that captures an Activity reference is a leak. Fifth: use lifecycle-aware components. LifecycleObserver, lifecycleScope, repeatOnLifecycle — these are designed to automatically clean up when the lifecycle ends.
Recap and What is Next
Here is what we covered. Objects reference other objects, forming a graph. The GC traces from roots, marking everything reachable as alive. If any path from a root reaches an object, it cannot be collected. Memory leaks happen when long-lived objects hold references to short-lived objects, preventing collection. Common Android leaks include Activity references in callbacks, ViewModels holding Views, and static Context references. LeakCanary detects leaks by watching for objects that survive past their expected lifetime, then dumps the heap to find the reference chain. WeakReferences let you hold a reference without preventing collection. The fundamental rule: think about lifetimes and reference direction. In the next video, we are going to look at caching — trading memory for speed — and why getting it wrong can actually make everything worse. See you there.