cswork / libraryOpen practice workspace

Articles

Android Memory Leak Interview Questions: Lifecycles, Retained Objects, and Heap Paths

Practice android memory leak interview questions with worked failure traces, recovery choices, and verification tests grounded in official documentation.

An Android memory leak interview answer should identify an object that has outlived its useful lifecycle, the strong reference path keeping it reachable, and the owner responsible for releasing that reference. A growing heap is a symptom, not a verdict. A large, intentionally retained cache and a destroyed Activity retained by a singleton need different fixes.

This guide uses official Android and LeakCanary documentation for platform behavior. The heap paths, code review, and regression protocol are original practice exercises, not captured device evidence or employer-reported questions. Start with Design Android MVVM API Architecture, then add a demanding follow-up: which references must disappear when the screen's view disappears?

What makes a retained object a leak?

Reachability and usefulness are different properties. Garbage collection can reclaim unreachable objects; it does not know that your old screen is no longer useful. A strong reference from a live owner can therefore preserve a perfectly valid object graph that violates your application lifecycle.

LeakCanary's fixing-a-leak guide explains how a trace connects garbage-collection roots to a retained object. Read the trace as ownership evidence rather than a list of classes to blame. The root is not automatically the bug, and the final object is not automatically the place to edit.

Ask three questions before proposing cleanup. What event ended the object's intended lifetime? How long after that event was the snapshot taken? Which remaining owner still has a legitimate reason to retain it? A temporarily queued callback might explain brief retention. Repeatedly accumulating destroyed views after callbacks should have finished supports a different conclusion.

Consider a photo application with a 40 MB bounded bitmap cache and five destroyed detail screens. Both consume memory. Only one necessarily violates the declared policy. You might still reduce the cache for low-memory devices, but call that capacity management, not proof of a lifecycle leak. Separating those hypotheses makes your next measurement useful.

Why does a Fragment binding need a shorter lifetime?

A Fragment and its view do not have identical lifetimes. A Fragment can remain on the back stack after its old view is destroyed. Android's view-binding documentation explicitly requires clearing the Fragment's binding reference in onDestroyView(). The Fragment lifecycle documentation describes the view's destruction separately from the Fragment's state.

Here is an original, abbreviated review example:

class ReceiptFragment : Fragment(R.layout.receipt) {
    private var binding: ReceiptBinding? = null

    override fun onViewCreated(view: View, state: Bundle?) {
        binding = ReceiptBinding.bind(view)
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                model.receipt.collect { receipt ->
                    binding?.total?.text = receipt.total
                }
            }
        }
    }

    override fun onDestroyView() {
        binding = null
        super.onDestroyView()
    }
}

The layout, binding, model, imports, and UI formatting are application-specific; this is an ownership sketch, not a standalone compiled project. Explain why the collector belongs to viewLifecycleOwner, why collection is gated on STARTED, and why the binding field still needs clearing. These address different references and behavior, not one universal magic line.

A reviewer should also inspect adapters, observers, delayed work, and callbacks registered outside that scope. Clearing one field does not remove a global registry's separate reference to the same view. Conversely, null-checking a binding can prevent a crash while leaving an incorrect long-lived subscription alive. Safe access and correct ownership are separate review questions.

Exercise: locate the first wrong ownership edge

Assume a receipt screen has been removed, its exit transition completed, and its view lifecycle reached DESTROYED. The following path is a synthetic fixture:

GC root
  → Application
  → ReceiptEventRegistry.listeners
  → ReceiptListener
  → capturedBinding
  → receiptRootView
  → destroyed ReceiptActivity

Application lifetime is expected. A registry also may be legitimate. A listener retaining a destroyed screen is where the policy becomes suspicious. The useful fix is usually to remove the listener when its view owner ends, or redesign the registration so that it does not capture UI objects beyond that boundary. Removing the Application reference is neither necessary nor sensible.

Write the ownership contract before editing: registration begins when this view is created; the subscription ends when that view is destroyed; late deliveries are ignored or canceled. If an event is valuable after navigation, route it into a repository or state holder containing data, not the old binding. The new view can render current state on return.

Now add a second retaining path: a delayed Runnable also captures capturedBinding. Removing the registry listener eliminates one path, but does not establish collectability. Find and cancel the owned Runnable, or remove its UI capture. A single displayed path can be sufficient to explain retention without being a complete list of every retaining reference.

The interviewer may ask why the view's context should not be cleared. That reference supports normal view behavior. Your job is to stop retaining the expired view from the long-lived component, not mutate framework internals until the diagram disappears. Fix the first application-owned edge that crosses the wrong lifetime boundary.

Does coroutine cancellation prove cleanup?

No. Cancellation addresses work owned by a coroutine scope, and cooperative code must actually observe it. It does not unregister arbitrary callbacks in an external API. Android's lifecycle-aware coroutine guidance is the starting point for selecting scopes; the concrete callback API determines how registration is released.

Imagine wrapping an event bus in callbackFlow. If collection ends but the wrapper never unregisters the listener, the bus can still hold the callback and its captured data. A complete answer discusses registration and cleanup together, including cancellation during startup, double cleanup, and emissions racing with shutdown. Do not promise that choosing a scope repairs an independently leaked subscription.

For a suspending network call, ask whether the underlying client cancels transport work and whether the callback retains an Activity. For application-level work that must continue, move its result ownership away from the screen. “Cancel everything when the user navigates” is wrong when the operation is an intentional durable upload. Keep the work, release its UI reference, and publish data through a screen-independent channel.

Avoid fetching a view into a local variable before a long delay and then assuming a later null binding protects it. That local variable may itself keep the old view alive. Review what the closure captures, not merely what the code accesses after the delay. The same distinction applies to anonymous inner classes and listeners.

Can a ViewModel, context, or weak reference solve the leak?

A ViewModel is not a safe container for every reference. Android's ViewModel guidance warns against retaining lifecycle-related objects such as views or Activity contexts. Its longer lifetime is useful for screen data across recreation, but dangerous for an old view tree.

Use a context whose lifetime and capabilities match the operation. Application context can be appropriate for some non-UI services; replacing an Activity context indiscriminately can change theming or fail APIs requiring an Activity. Explain the requirement rather than reciting “always use application context.” Prefer passing narrow data or an explicitly scoped interface when possible.

A weak reference changes retention, not correctness. It may prevent one owner from keeping an object alive, but introduces a nullable result that can disappear between events. It can also conceal an unbounded registry of dead listener wrappers. If the owner should unsubscribe, implement that contract first. Use weak ownership only where disappearance is an intentional part of the API design.

Similarly, a cache needs an explicit budget, eviction policy, and key semantics. A memory leak exercise about destroyed screens should not turn into arbitrary cache eviction. You can have both issues at once: a valid cache that is too large and a leaked Activity. State which change addresses which hypothesis and how each will be measured independently.

How do heap paths differ from memory graphs?

A time-series graph can show growth, drops after collection, or allocation bursts. It cannot tell you whether a retained object is still needed. A heap dump lets you inspect objects and relationships at a point in time. Android Studio's heap-dump guide covers capture and analysis, including instance and retained-size information.

Distinguish shallow size from retained size. A listener may be tiny yet dominate a large UI subtree. Conversely, the largest object may be healthy cached data, not the policy-breaking owner. State the sample timing and reproduction actions so another engineer can compare the same lifecycle point after your fix.

Do not claim every byte visible in process memory is explained by a managed heap dump. Native allocations, graphics resources, and other categories may need separate tools. If managed destroyed-screen instances stay flat while process memory grows, revise the hypothesis instead of forcing a Java leak explanation. Capture methods also affect application timing and should not be mistaken for normal performance measurements.

One useful interview sentence is: “The graph identifies when to investigate; the path identifies who can retain the object; the lifecycle contract tells me whether that retention is wrong.” Then name the actual edge and the smallest correction you would test.

Build a regression test that can disprove your fix

Use this original protocol for the receipt example. Establish a baseline after the home screen is idle. Open a receipt, register its listener, navigate away, and wait for the declared cleanup and transition boundary. Repeat a fixed number of cycles and inspect destroyed receipt views and outstanding registry registrations at the same checkpoint.

Repeat with rotation, rapid back navigation, an event arriving during destruction, a failed fetch, and a slow callback. Include a healthy-screen control: the currently visible receipt must remain functional and reachable. A test that makes every view disappear by canceling all subscriptions may hide the leak by breaking the feature.

Record both expected owner cleanup and actual heap evidence. Registry size should return to its baseline; old receipt views should not accumulate after the detector's collection/analysis window. A single successful collection is encouraging, but repeated reproductions are stronger evidence. Absence of a LeakCanary alert alone is not proof that every memory category is healthy.

If a late event can race with cleanup, specify thread confinement or synchronization. Cleanup that occasionally removes the wrong listener can pass a slow manual test while leaking under rapid navigation. Give each registration its own identity, make teardown safe to repeat, and verify that a newly created screen's listener is not accidentally removed with its predecessor.

Sources and further reading