Java/Kotlin's reference types control when the GC can collect an object. Using the right reference type in the right place prevents both memory leaks and premature collection.
Reference Types Overview
| Reference | GC behavior | Android use case |
|---|---|---|
| Strong (normal) | Never collected while referenced | Everything by default |
WeakReference | Collected at next GC cycle | Observer patterns, caches where stale is OK |
SoftReference | Collected only when heap is low | Memory-sensitive caches |
PhantomReference | Collected; get() always returns null | Resource cleanup (advanced) |
WeakReference: Breaking Retain Cycles
Use WeakReference when a long-lived object needs to reference a short-lived one without preventing its collection.
// ❌ AsyncTask (conceptual) holds strong reference — Activity leaked if task runs long
class MyTask(private val activity: MyActivity) : AsyncTask<Unit, Unit, Result>() {
override fun onPostExecute(result: Result) {
activity.updateUI(result) // might run after Activity destroyed
}
}
// ✅ WeakReference — if Activity is destroyed, reference is null, we bail out safely
class MyTask(activity: MyActivity) : AsyncTask<Unit, Unit, Result>() {
private val activityRef = WeakReference(activity)
override fun onPostExecute(result: Result) {
activityRef.get()?.updateUI(result) // null-safe: Activity may be gone
}
}
WeakReference in Callback Registrations
class EventBus {
// Store listeners as WeakReferences so they don't prevent GC
private val listeners = mutableListOf<WeakReference<EventListener>>()
fun register(listener: EventListener) {
listeners.add(WeakReference(listener))
}
fun dispatch(event: Event) {
val iterator = listeners.iterator()
while (iterator.hasNext()) {
val listener = iterator.next().get()
if (listener == null) {
iterator.remove() // clean up collected references
} else {
listener.onEvent(event)
}
}
}
}
SoftReference: Memory-Sensitive Cache
SoftReference is suitable for caches where the cached object can be reconstructed but you want to keep it as long as memory allows.
class SoftBitmapCache {
private val cache = HashMap<String, SoftReference<Bitmap>>()
fun put(key: String, bitmap: Bitmap) {
cache[key] = SoftReference(bitmap)
}
fun get(key: String): Bitmap? {
val ref = cache[key] ?: return null
val bitmap = ref.get()
if (bitmap == null) cache.remove(key) // GC already collected it
return bitmap
}
}
Warning: In practice, SoftReference on Android can be too aggressive. The Dalvik/ART VM may clear soft references earlier than expected. Prefer LruCache with a fixed size for production bitmap caches — it's more predictable.
When NOT to Use Weak/Soft References
| Scenario | Better approach |
|---|---|
| ViewModel referencing Activity | Never — ViewModel must not hold Views |
| Background thread → UI update | lifecycleScope; it cancels on destroy |
| Coroutine holding Context | Use applicationContext in non-UI coroutines |
| Cache where correctness matters | LruCache with explicit eviction |
Common Pitfall: WeakReference + Coroutine
// ❌ The WeakReference may be null by the time the coroutine resumes
suspend fun doWork(activity: MyActivity) {
val weakRef = WeakReference(activity)
delay(5000)
weakRef.get()?.updateUI() // might be null and confusing
}
// ✅ Use lifecycleScope — it cancels automatically when Activity is destroyed
class MyActivity : AppCompatActivity() {
fun startWork() = lifecycleScope.launch {
delay(5000)
updateUI() // if Activity is destroyed, scope is cancelled — no crash
}
}
Key Takeaways
- Use
WeakReferenceto break retain cycles between long-lived objects and short-lived ones - Use it in listener/callback registrations where the listener is also the registrant's lifecycle
- Prefer
lifecycleScope/viewModelScopeoverWeakReferencefor coroutines — they're cleaner - Use
SoftReferenceonly for caches where you want heap-pressure-driven eviction; preferLruCachefor deterministic sizing - Always null-check
weakRef.get()— it can become null at any GC cycle