LeakCanary Says Where, LLM Says Why: Prompting Your Way Out of OutOfMemoryError in Android

Your app crashes in production with an OutOfMemoryError, and LeakCanary points at an Activity that never got garbage‑collected. Great — you know where the leak lives, but not why it exists. That's the frustrating part: the stack trace shows the symptom, not the cause. In this roundup, I'll show you how to feed LeakCanary's output to an LLM and get actionable answers: the static context, the unclosed Cursor, the listener stuck in a Singleton. Each prompt comes with a real trace example and the kind of answer you should expect. The goal is to turn a vague crash report into a concrete fix — and a test that proves it.

When LeakCanary Is Enough, and When You Need an LLM

LeakCanary excels at detecting leaks, but it doesn't explain root causes. For simple cases — like forgetting to unregister a BroadcastReceiver — the trace is self‑explanatory. But when the leak involves a chain of references through a Singleton, a Handler, or a Cursor that's held by a static field, the trace becomes a puzzle. That's where an LLM helps: it can read the object graph and suggest plausible causes, then help you refactor. More on the topic: “Go, LLM, and the Race Against Races”.

A common mistake: developers copy the entire LeakCanary trace into ChatGPT and ask "fix this." The model gets overwhelmed by noise and produces generic advice. Instead, extract the key leak trace (the reference chain) and give the LLM context about your architecture. Below are prompts that work.

The real value of an LLM here It's not about finding the leak — LeakCanary does that. It's about interpreting the reference chain and generating a targeted fix. Treat the LLM as a senior developer who's seen this pattern before.

Prompt 1: Diagnose the Reference Chain

Use this when LeakCanary shows a clear chain but you can't spot the culprit. The prompt asks the LLM to analyze the chain and list the most likely cause.

Task: Identify the root cause from a LeakCanary reference chain.

Prompt:

You are an Android memory leak expert. Below is a LeakCanary reference chain for an Activity leak. Analyze it and tell me: 1) the most likely cause (static context, unclosed resource, listener in Singleton, etc.), 2) the exact line in code where the leak originates, 3) a suggested fix. Be specific — don't give generic advice. More on the topic: “[Kotlin and Android Development](/en/blog/kotlin-and-android-development-the-myths-that-cost-beginners-a-year-and-how-the-stack-actually-works)”.

LeakCanary trace:
┬───
│ GC Root: Global variable in native code
│
├─ android.os.Handler instance
│ Leaking: NO (MessageQueue not leaking)
│ ↓ Handler.callback
├─ com.example.MyActivity$1 instance
│ Leaking: YES (MyActivity is leaking)
│ ↓ MyActivity$1.this$0
╰→ com.example.MyActivity instance
Leaking: YES (Activity is leaking)

Context: MyActivity uses a Handler to post delayed tasks. The Handler is created as an anonymous inner class.

Expected answer: The LLM should point out that the anonymous Handler holds an implicit reference to MyActivity. If the Handler posts a delayed message that outlives the Activity, the message queue keeps the Activity alive. Fix: use a static Handler with a WeakReference, or cancel messages in onDestroy().

This prompt works because it gives the LLM the chain and a hint about the Handler. Without the hint, the model might still guess correctly, but the hint reduces hallucinations.

Prompt 2: Refactor to WeakReference

Once you know the cause, ask the LLM to generate the refactored code. This is especially useful for Handlers, callbacks, and listeners that hold Activity references.

Task: Refactor a leaking Handler to use WeakReference.

Prompt:

Here's my current code that leaks an Activity:

 public class MyActivity extends AppCompatActivity {
 private final Handler handler = new Handler(Looper.getMainLooper()) {
 @Override
 public void handleMessage(Message msg) {
 updateUI();
 }
 };

 private void updateUI() { /*... */ }
 }

Refactor it to avoid the leak using a static Handler with a WeakReference. Show the full code and explain what changed. More on the topic: “Mobile Development”.

Expected answer: The LLM should produce a static inner class that holds a WeakReference to the Activity, check for null before using it, and mention that you should also remove callbacks in onDestroy(). Example:

private static class SafeHandler extends Handler {
 private final WeakReference<MyActivity> activityRef;
 SafeHandler(MyActivity activity) {
 super(Looper.getMainLooper());
 activityRef = new WeakReference<>(activity);
 }
 @Override
 public void handleMessage(Message msg) {
 MyActivity activity = activityRef.get();
 if (activity!= null) {
 activity.updateUI();
 }
 }
}

And in onDestroy: handler.removeCallbacksAndMessages(null);

This is standard Android practice, but the LLM saves you typing and reminds you about cleanup.

Prompt 3: Generate a Reproducer Test

You've fixed the leak, but how do you prove it? Ask the LLM to write a unit test that reproduces the leak using Robolectric or instrumentation. This is where many developers stop — they fix and hope. A test prevents regressions.

Task: Write a test that detects the Activity leak.

Prompt:

Write a Robolectric test that verifies MyActivity does not leak after being destroyed. Use the LeakCanary test artifact if needed. The test should: 1) launch the Activity, 2) trigger the Handler message, 3) destroy the Activity, 4) assert that the Activity is garbage‑collectable. Provide the full test code and any Gradle dependencies.

Expected answer: The LLM should suggest using LeakCanary.install() in the test, or a simpler approach with WeakReference and System.gc(). A typical test:

@Test
public void activityDoesNotLeak() {
 MyActivity activity = Robolectric.buildActivity(MyActivity.class).setup().get();
 Handler handler = activity.getHandler();
 handler.sendEmptyMessage(0);
 WeakReference<MyActivity> ref = new WeakReference<>(activity);
 activity = null;
 System.gc();
 assertNull(ref.get());
}

Note: Robolectric tests may not perfectly mimic real GC, but they catch obvious leaks. For more reliable detection, use instrumentation tests with LeakCanary's FailTestOnLeakRunListener.

Prompt 4: Verify with Memory Profiler

Finally, confirm the fix using Android Studio's Memory Profiler. The LLM can guide you through the steps and interpret the heap dump.

Task: Interpret a heap dump to confirm the leak is gone.

Prompt:

I suspect MyActivity is still leaking after my fix. I took a heap dump in Android Studio Memory Profiler. The dump shows 3 instances of MyActivity, but only 1 should exist. How do I find the GC root for the extra instances and confirm the leak? Give me step‑by‑step instructions.

Expected answer: The LLM should explain how to use the "Analyzer Tasks" in Memory Profiler to detect leaked activities, how to look at the reference tree, and how to identify the GC root. It might also suggest using adb shell dumpsys meminfo for a quick check. The key is to filter by class name and examine the dominator tree.

Don't trust the first answer LLMs sometimes hallucinate Android APIs. Always verify the suggested code compiles and that the test actually fails before the fix and passes after. Treat the LLM as a pair programmer, not an oracle.

Where This Approach Breaks

These prompts work best when you provide enough context: the trace, the relevant code, and a hint about your architecture. They fail when the leak is caused by a third‑party library or a complex interaction between components. In those cases, the LLM might suggest plausible but wrong fixes. Always cross‑check with official documentation and your own debugging. More on the topic: “Mobile Development”.

Another limitation: LLMs can't run your app or inspect memory. They rely on the data you give them. If the trace is incomplete or you omit the context, the answer will be vague. Garbage in, garbage out.

For related debugging techniques, see The LLM Detective: DevTools Prompts That Reproduce Bugs, Read Stack Traces and Set Breakpoints Instead of Guessing and 12 LLM Prompts for Mobile Release Day. More on the topic: “Flutter and Dart”.

The Takeaway

You don't need to guess why your app is leaking. With the right prompts, an LLM can turn a cryptic LeakCanary trace into a clear diagnosis, a refactored fix, and a test that keeps the leak from coming back. Start with the reference chain, ask for specifics, and always verify the output. Your future self — and your users — will thank you. More on the topic: “Your Goroutines Don't Leak”.

← All posts

Ask AI about this article

Ask a question about this article — ASI Biont will answer based on its text.