Opening

We have talked about how the garbage collector traces from roots, following references to decide what stays alive. But so far, we have treated all references as equal. They are not. The JVM actually has four different types of references, and each one tells the GC something different about how much you care about keeping that object alive. Understanding these reference types gives you fine-grained control over memory — the ability to say "keep this if you can, but let it go if you must." Today we are going to explore strong references, weak references, soft references, and phantom references. We will look at when each one is useful, why SoftReference for image caching used to be the standard Android pattern and why it is usually wrong now, and how tools like WeakHashMap and ReferenceQueue work under the hood. You almost never need to use these directly — but you absolutely need to understand them, because the libraries and frameworks you rely on use them constantly.

Strong References: The Default

Let us start with what you already know, but put a name on it. Every normal reference in your code is a strong reference. When you write val user = User("Alice", 30), the variable user holds a strong reference to that User object. Strong means: the GC cannot collect this object as long as this reference exists and is reachable from a GC root. Period. No negotiation. No conditions. The GC will never, under any circumstances, collect a strongly referenced object. It does not matter if the system is critically low on memory. It does not matter if the object has not been accessed in hours. As long as there is a path of strong references from a GC root to the object, it lives. This is why we have memory leaks — a forgotten strong reference keeps an object alive indefinitely. Strong references are the bricks of the object graph. Everything we discussed about memory leaks and GC tracing in the previous videos was about strong references.

WeakReference: GC Can Collect Anytime

A WeakReference is the opposite extreme. It says to the GC: I have a reference to this object, but I do not need it to stay alive for my sake. If the only remaining references to an object are weak references, the GC is free to collect it at the next collection cycle. Not under memory pressure. Not when it feels like it. At the very next GC. It is as if the weak reference is invisible to the GC's tracing algorithm. In code, you create one like this: val ref = WeakReference(myObject). To access the object, you call ref.get(). If it returns the object, great — it has not been collected yet. If it returns null, the GC already reclaimed it. You must always check for null. This is the tool you use when you want to observe an object without prolonging its life. Remember the Activity leak pattern — a callback manager holding a strong reference to an Activity? If that reference were a WeakReference instead, the Activity would be collected normally when it is destroyed, and the manager would get null when it tried to use it.

SoftReference: GC Collects Under Pressure

SoftReference sits in between. It says to the GC: I would like this object to stick around if possible, but if you are running low on memory, you can collect it. The GC treats soft references differently from weak references. A weakly referenced object can be collected at any GC cycle. A softly referenced object is guaranteed to be collected before the JVM throws an OutOfMemoryError, but the GC will try to keep it around as long as memory is plentiful. Think of it as a memory-sensitive cache. The object stays alive when memory is abundant, giving you cache-hit benefits. When memory gets tight, the GC reclaims it, freeing up space. You do not have to manage the eviction yourself — the GC does it for you based on actual memory pressure. In practice, the exact behavior varies between JVM implementations and Android runtime versions. Some implementations are more aggressive about clearing soft references than others. This unpredictability is part of why SoftReference has fallen out of favor for certain use cases.

PhantomReference: For Cleanup Only

The fourth type is PhantomReference, and it is the strangest one. You can never access the object through a PhantomReference — get() always returns null. So what is the point? PhantomReferences are a notification mechanism. They tell you after an object has been finalized and is about to be reclaimed. You use them when you need to perform cleanup actions — releasing native resources, closing file handles, deallocating off-heap memory — that depend on knowing when an object is truly dead. Here is how it works. You create a PhantomReference and associate it with a ReferenceQueue. When the GC determines that the object is phantom-reachable — meaning no strong, soft, or weak references exist — it enqueues the PhantomReference into the queue. Your cleanup code polls the queue and performs the necessary cleanup. The object itself is already gone — you cannot access it. But you know it is gone, and you can clean up associated resources. In practice, most Android developers never create PhantomReferences directly. They are used internally by the runtime and by libraries that manage native resources. But knowing they exist helps you understand the full picture of how the GC manages object lifecycles.

The Reference Hierarchy

Let me lay out the hierarchy so you can see how the GC prioritizes. An object is strongly reachable if there is a path of strong references from a GC root. It will never be collected. An object is softly reachable if the strongest path to it goes through a SoftReference. It will be collected under memory pressure. An object is weakly reachable if the strongest path to it goes through a WeakReference. It can be collected at any GC cycle. An object is phantom-reachable if it has been finalized but a PhantomReference exists. It cannot be accessed but cleanup can happen. An object is unreachable if no references of any kind exist. It is collected. The key word is "strongest." If an object has both a strong reference and a weak reference pointing to it, it is strongly reachable — the strong reference wins. The weaker reference types only matter when they are the only references keeping the object reachable.

Use Case: WeakReference for Leak Prevention

The most common use case for WeakReference in Android is preventing memory leaks in callback patterns. Say you have a network manager that holds references to listeners. If those listeners are Activities or Fragments, strong references will cause leaks. Instead, the manager stores WeakReference wrappers. When it is time to notify listeners, it iterates through the list, calls get() on each, and invokes the callback if the reference is still alive. If get() returns null, it removes that entry from the list. This pattern lets the Activities and Fragments be collected normally by the GC. The manager gracefully handles their disappearance. No leaks, no crashes. Just a null check. You see this pattern in many Android libraries. Event buses, observable implementations, and callback registries often use weak references internally to avoid creating accidental strong reference chains to short-lived components.

Use Case: SoftReference for Caches (and Why It Is Usually Wrong)

Here is a piece of Android history that is still relevant today. In early Android development, the recommended pattern for image caching was to use SoftReferences. You would load a bitmap, wrap it in a SoftReference, and store it in a HashMap. When you needed the image again, you would check the SoftReference. If the GC had not reclaimed it — great, cache hit. If it had — cache miss, reload the image. This seemed elegant. The GC managed your cache size automatically based on memory pressure. But it turned out to be a terrible strategy in practice. The problem is that Android's GC — especially in ART — tends to be aggressive about clearing soft references. In many situations, the GC would clear your image cache even when memory was not critically low, just because it ran a collection cycle. Your "cache" was being emptied constantly, giving you almost no cache hits. You had all the complexity of a cache with almost none of the benefit. The modern approach — used by Coil, Glide, and Picasso — is to use a proper LruCache with an explicit size limit. You control the eviction policy. You get predictable behavior. You can tune the cache size based on your app's needs. SoftReference caches are unpredictable and usually underperform compared to a well-configured LruCache.

WeakHashMap

WeakHashMap is a specialized HashMap where the keys are held by weak references. When a key is garbage collected, the entry is automatically removed from the map. This is useful for associating metadata with objects without preventing their collection. For example, you might want to track which objects have been processed by attaching a flag. You store the object as a key in a WeakHashMap with true as the value. As long as the object is alive, the entry exists. When the object is collected, the entry disappears. No manual cleanup needed. But there is a gotcha. WeakHashMap holds keys weakly but values strongly. If your value holds a strong reference back to the key — say, you store an object as a key and a wrapper containing that object as a value — the key can never be collected because the value keeps it alive through a strong reference chain. This circular pattern defeats the purpose of WeakHashMap. Always make sure your values do not reference your keys.

ReferenceQueue: Getting Notified

A ReferenceQueue is how you find out that a referenced object has been collected. When you create a WeakReference or SoftReference, you can optionally pass a ReferenceQueue. When the GC collects the referenced object, it enqueues the Reference object itself into the queue. You can then poll the queue to discover which references have been cleared. LeakCanary uses this exact mechanism. It creates a WeakReference to a destroyed Activity with a ReferenceQueue. If the Activity is properly collected, the WeakReference appears in the queue. If it does not appear — the Activity was not collected — LeakCanary knows there is a leak. You can build your own cleanup mechanisms with ReferenceQueue. A common pattern is a background thread or coroutine that polls the queue periodically and cleans up resources associated with collected objects — removing entries from maps, closing native handles, or releasing buffers. It is a powerful low-level primitive for resource management.

Why You Almost Never Need These

Let me be direct. In modern Android development, you will rarely create WeakReference or SoftReference yourself. Here is why. For preventing leaks, lifecycle-aware components do the job better. ViewModelScope, lifecycleScope, repeatOnLifecycle — these automatically cancel work when the lifecycle ends, eliminating the need for weak reference callbacks. For caching, LruCache with explicit size management outperforms SoftReference caches every time. For resource cleanup, use Closeable and use blocks, or AutoCloseable, or try-with-resources patterns. These are more predictable than PhantomReference-based cleanup. The reference types still matter, though. Libraries you depend on use them internally. LeakCanary, as we discussed, uses WeakReference and ReferenceQueue. Glide uses weak references for bitmap tracking. The Android framework itself uses them in various places. Understanding these reference types helps you understand your tools, read library source code, diagnose GC-related issues, and make informed decisions when you are working at the edge of normal Android development.

Recap and What is Next

Let us summarize. Strong references are the default — the GC never collects strongly reachable objects. WeakReference means the GC can collect anytime — use it for leak-safe callbacks and observation without ownership. SoftReference means the GC collects under memory pressure — conceptually a memory-sensitive cache, but unreliable on Android in practice. PhantomReference is for post-mortem cleanup — you cannot access the object, just know it is gone. WeakHashMap holds keys weakly for automatic entry removal. ReferenceQueue notifies you when referenced objects are collected. And the practical advice: prefer lifecycle-aware components over WeakReference, prefer LruCache over SoftReference, and prefer Closeable over PhantomReference. But understand all of them, because the code you depend on uses them daily. That wraps up Phase 3 — Data and Memory. You now understand how mutability works at the byte level, how object graphs cause leaks, how caching trades memory for speed, how strings secretly dominate your allocations, and how the GC sees different reference types. In the next phase, we will explore how your app talks to the outside world — networking, serialization, and I/O.