Memory leaks cause apps to slow down and crash after long use. A leak happens when an object stays in memory after the app no longer needs it. Repeated leaks grow memory use until the system kills the app. This guide shows how to find leaks with Profiler and fix them at the source. The focus is on practical steps that you can repeat for any app.
Start with a clear reproduction of the leak. Open a screen and leave it several times. Watch memory use after each visit. A healthy app returns to a similar memory level after each visit. A leaking app shows a steady rise that does not fall back. A clear reproduction makes the leak easy to study.
Record the baseline memory level before you start the reproduction. Record the level after each screen visit. A table of levels shows the growth pattern clearly. A sudden jump points to a large object leak. A slow rise points to many small objects that accumulate over time. Both patterns need different fixes.
Capture A Heap Dump At The Right Moment
A heap dump records all objects in memory at one moment. Capture a dump after the reproduction shows growth. Capture a second dump after more repetitions to compare. The comparison shows which object types grew most. Focus on the types with the largest growth first because they cause the most pressure.
Open the heap dump in Profiler and sort by retained size. Retained size shows memory that an object keeps alive through its references. A large retained size for an activity means that activity cannot be freed. Follow the reference chain from the activity to the object that holds it. The chain reveals the leak source.
- Reproduce the leak with repeated screen visits
- Record memory levels after each visit
- Capture heap dumps before and after growth
- Sort by retained size to find large holders
- Follow the reference chain to the source
Read Reference Chains Carefully
A reference chain shows the path from a root object to the leaked object. A root object stays alive for the whole app lifetime. A static field is a common root. A long-lived manager can also act as a root. If a short-lived activity links to a root through any path then the activity cannot be freed. Find the link that connects the activity to the root.
Look for listener registrations in the chain. A listener that is never removed keeps its owner alive. A callback stored in a manager keeps the screen alive after the user leaves. A background task that holds a screen reference has the same effect. Remove the registration or clear the reference when the screen leaves. A clean lifecycle prevents most listener leaks.
Listener leaks also appear in Compose work covered in the material on Jetpack Compose interface tweaks. A composable that registers a listener must unregister it when it leaves composition. A state holder that outlives the screen must not hold a screen reference. Check lifecycle boundaries in both classic views and Compose screens.
Fix Common Leak Patterns
Static fields that hold a context are a classic leak pattern. A static field lives for the whole app lifetime. Storing an activity in a static field keeps that activity alive forever. Use an application context for static needs when a context is truly required. Better still avoid storing a context in a static field at all.
Inner classes can also leak their outer class. A non-static inner class holds a reference to its outer class. A background task defined as an inner class can keep a screen alive after the user leaves. Use a static inner class with a weak reference when a task must outlive the screen. A weak reference allows the screen to be freed when the user leaves.
Test Fixes With Repeated Flows
Test a fix by repeating the original reproduction exactly. Open and leave the screen the same number of times. Compare memory levels before and after the fix. A successful fix returns memory to near baseline after each visit. A partial fix slows growth but does not stop it. A partial fix means another leak path remains.
Test the fix on a low memory device when possible. A low memory device triggers system pressure sooner and reveals leaks faster. A fix that works on a high memory device may still leak on a low memory device. Test both device classes before you declare the leak fixed.
Use Leak Detection During Development
Leak detection tools can watch for leaked activities during normal testing. They alert you when an activity stays alive after it should be freed. An early alert during development is cheaper than a crash report from users. Add leak detection to debug builds only. Remove it from release builds to avoid extra overhead.
Respond to every leak alert during development. An alert that you ignore becomes a crash later. Investigate the reference chain for each alert and fix the source. A team that fixes leaks early builds apps that stay stable during long sessions. Stability is a feature that users notice even when they cannot name it.
Monitor Memory In Production Safely
Production monitoring should track crash rates and memory related crashes without collecting personal data. A rise in out of memory crashes after a release points to a new leak. Compare crash rates across versions to find the release that introduced the leak. A version comparison narrows the search to recent changes.
Use crash reports to guide reproduction in the lab. A report that names a screen and a flow gives you a starting point. Reproduce that flow in the lab and capture heap dumps. A lab reproduction turns a vague crash report into a concrete fix. Close the loop by testing the fix against the same flow.
heap_dump_1 = before_repeat
heap_dump_2 = after_repeat
leaked_type = Activity
retained_size = large
The snippet above is an original example written for this guide. It shows generic dump labels that you can adapt to your own test notes. It does not copy code from any outside source.
Use the labels to name your dump files clearly. A clear name helps you compare dumps without opening each one. Keep dump files in a test folder that you can delete after the fix.
Share Heap Dump Findings With The Team
Heap dump findings help the team only when they are shared clearly. A finding should name the leaked object and the reference chain that holds it and the fix that removes the link. A vague finding about memory growth helps no one. A specific finding with a chain gives the author a clear fix. Share findings in a short document with screenshots of the chain when possible. A visual chain is easier to follow than a text description alone.
Track leak fixes in the project issue tracker. An issue should link to the heap dump and to the fix commit. A linked issue lets future developers see why a code change happened. It also prevents a later refactor from reintroducing the same leak. A refactor that touches the fixed code should check the linked issue first. History prevents repeat mistakes.
Review leak fixes in code review with the heap dump open. A reviewer can confirm that the fix breaks the exact link in the chain. A review without the dump may approve a fix that addresses a different link. Evidence based review is stronger than review from memory. Keep dumps for fixed leaks in an archive for a short period so reviewers can consult them.
Shared heap findings should also include the exact steps to reproduce the leak. A reproduction list should state the screen and the actions and the number of repetitions. A teammate who follows the list should see the same growth. A reproduction that works only for its author is not a reliable finding. Clear steps turn a personal observation into team evidence.
Archive heap dumps for important leaks in a shared folder with a retention policy. A dump archive helps a future investigation of a similar leak. A retention policy prevents the archive from growing without limit. Delete dumps for fixed leaks after a few releases unless the leak is likely to return. A tidy archive stays useful.
Archive retention should be reviewed when storage grows. A folder full of old dumps can slow a shared drive. Delete dumps that belong to fixed leaks after a few releases. Keep a short summary of each deleted dump in the issue tracker. A summary preserves the lesson without preserving the large file.
Common Mistakes
A common mistake is capturing a heap dump too early. An early dump shows normal objects and hides the leak. Wait until repeated flows show clear growth before you capture. A dump at the right moment reveals the leak clearly.
Another mistake is fixing a symptom by forcing garbage collection. Forcing collection hides growth temporarily but it does not remove the reference that causes the leak. Fix the reference chain at the source. A source fix keeps memory stable without manual collection.
Some developers also blame the system for a leak in their own code. A system kill after long use usually means the app held too much memory. Check your own reference chains first. A code fix is usually available.
Finally do not ignore small leaks because they look harmless. Small leaks accumulate during long sessions. A small leak that grows slowly can still crash an app after hours of use. Fix small leaks with the same care as large leaks.
Frequently Asked Questions
How do I know if memory growth is a leak?
Growth that falls back after a flow ends is normal. Growth that rises steadily with repeated flows and never falls back points to a leak. Compare levels across repetitions to judge.
What is retained size?
Retained size is the memory that an object keeps alive through its references. A large retained size for a screen means that screen cannot be freed. Follow the chain to find the holder.
Can Profiler fix leaks automatically?
No Profiler shows objects and reference chains but it does not change code. You must fix the reference that holds the leaked object. Profiler guides the fix but it does not apply it.
Conclusion
Memory leaks become manageable when you reproduce them and read reference chains carefully. Capture heap dumps at the right moment and fix the link that holds the leaked object. Test the fix with repeated flows on realistic devices. Your concrete next step is to reproduce a leak in your own app today by opening and leaving one screen five times. Record memory levels and capture a heap dump if growth persists. For related work you can study Kotlin performance fixes and Jetpack Compose interface tweaks and Android NDK optimizations as next steps.


