Finding memory leaks, UI jank, and performance problems with Android Studio
The rest of this guide explained the concepts — garbage collection, leaks, jank, threading. This appendix is about finding those problems in a real app using the tooling built into Android Studio (itself based on IntelliJ IDEA). These are diagnostic skills: you reach for them when memory is climbing, frames are dropping, or the app freezes, and they turn the abstract ideas from earlier chapters into things you can measure. The tools evolve release to release, so treat the specifics as a current snapshot and the workflow as the durable part.
The Android Studio Profiler
The Android Studio Profiler is a suite of real-time performance tools that replaced the older Android Monitor. It surfaces four dimensions of an app’s runtime behaviour — memory, CPU, network, and energy — while the app runs on a device or emulator. You open it via View > Tool Windows > Profiler, then select the running app’s process to start a session.
| Profiler | Diagnoses | Maps to chapter |
|---|---|---|
| Memory Profiler | Leaks, allocation churn, GC pressure | Garbage Collection, Memory |
| CPU Profiler | UI jank, slow methods, thread contention | Lifecycle, Coroutines |
| Network Profiler | Excessive or slow network calls | N+1 Problem |
| Energy Profiler | Battery drain from wakelocks/work | Lifecycle |
Warning:
Profiling accuracy depends on build type. Allocation tracking and heap dumps require a debuggable build, which disables compiler optimizations and adds overhead — great for finding leaks, but not for measuring real performance numbers. Measure speed on a release-style build; hunt leaks and allocations on a debuggable one.
Memory Profiler — Hunting Leaks
The Memory Profiler is the primary tool for the leaks discussed in the Garbage Collection chapter. It shows a live graph of how much memory the app uses, how many Java objects are allocated, and — critically — when garbage collections occur. Reading that timeline is the first diagnostic skill.
Reading the timeline and the time-between-GCs
A healthy app shows memory rising as objects are allocated, then dropping at each GC, in a sawtooth pattern. Two signals indicate trouble: a baseline that keeps climbing across GCs (memory not being reclaimed — a likely leak), and GCs happening very frequently (high allocation churn pressuring the collector). A concrete, measurable win after a memory optimization is a longer time between GCs: fewer collections mean less CPU contention, which in turn means less jank and fewer out-of-memory kills.
Heap dumps
A heap dump is a snapshot of every object on the Java heap at a moment in time — the tool to confirm and locate a leak. The workflow: exercise the suspected flow (for an Activity leak, rotate the screen or navigate away and back several times), force a GC from the profiler, then capture a heap dump. Anything that should have been collected but is still present is your suspect. Because the dump runs in the app’s own process and needs memory to collect, a brief spike during capture is normal.
Interview Tip:
Classic confirmation of the leak from the GC chapter: rotate a leaking screen ten times, force GC, capture a heap dump, and look for ten retained Activity instances. A correctly written screen shows one (or zero); ten destroyed-but-retained Activities is the leak made visible.
Allocation tracking and the reference chain
Two views turn ‘something leaked’ into ‘this line of code leaked it’. Allocation tracking records where objects are being created, with a full stack trace for each allocation — ideal for finding the hot path that churns throwaway objects and pressures the GC. Inside a heap dump, the Instance Details pane has Fields and References tabs: References shows what is still pointing at the leaked object, letting you walk the chain back to the GC root that pins it (the static field, the singleton, the un-unregistered listener). That chain is exactly the ‘reachable from a root’ situation the GC chapter described — here you can see it concretely.
LeakCanary — Automatic Leak Detection
LeakCanary is a library that watches objects which should have been collected (destroyed Activities, Fragments, ViewModels) and, when one is retained too long, automatically captures a heap dump and reports the full reference chain from the GC root to the leak — the analysis the manual heap-dump workflow produces, done for you on every leak as you develop. It is added as a debug-only dependency so it never ships in release builds.
Beginning with the Android Studio ‘Panda’ release, the Profiler integrates LeakCanary as a dedicated task. This moves the leak analysis from the test device to your development machine, which is a significant performance improvement over analysing on-device, and keeps the detection and the profiler in one place.
Interview Tip:
Workflow that catches most leaks early: run a debug build with LeakCanary while you develop, and whenever you finish a screen, navigate away and rotate a few times. If a destroyed Activity or ViewModel is retained, LeakCanary surfaces the chain immediately — before the leak ever reaches a user.
CPU Profiler & System Trace — Hunting Jank
Jank is a dropped frame: the UI thread did not finish its work within the frame budget (about 16 ms at 60 Hz), so the screen stutters. The CPU Profiler diagnoses it. Its System Trace recording is the right mode for jank: it shows how your app uses the CPU cores, how threads are scheduled, where frames are rendered, and where the rendering pipeline stalls. You navigate the trace with the WASD keys (pan and zoom) and select a time range to focus on a janky moment.
- Main-thread work: the most common jank cause — heavy computation, disk, or network on the UI thread. The fix is to move it off-thread (the Coroutines chapter), and the trace shows which method is blocking.
- GC during scrolling: as the GC chapter noted, collections cause CPU contention that defers rendering. If jank lines up with GC events, the real fix is reducing allocation in the scroll path, not the rendering code.
- Startup time: for cold-start analysis specifically, System Trace plus the Macrobenchmark library is the standard combination.
The CPU Profiler is also where you confirm threading behaviour from the Coroutines chapter: you can see which dispatcher’s threads are doing work, whether the main thread is idle during I/O (as it should be), and whether background work is actually running off the UI thread.
Android Vitals — Production Metrics
Profiling on your machine catches what you can reproduce; Android Vitals (in the Google Play Console) catches what happens on real users’ devices at scale. It aggregates stability and performance metrics reported from the field — you can filter them, compare against peer apps, and track a metric over a long window (up to three years).
| Metric | What it captures |
|---|---|
| ANRs | ‘Application Not Responding’ — the UI thread blocked too long |
| Crashes | Uncaught exceptions, with stack traces and affected devices |
| Excessive wakeups | Background work draining battery |
| LMK rate | Low-memory kills — how often the system kills your app for RAM |
The LMK rate ties directly back to the Garbage Collection chapter: an app under constant memory pressure is killed more often when backgrounded, costing you warm starts and lost state. Reducing memory pressure — measured locally as longer time-between-GCs — shows up in production as a lower LMK rate. ANRs tie back to Lifecycle and Coroutines: they are almost always main-thread work that should have been moved off it.
A Diagnostic Workflow
Putting the tools together, the path from symptom to fix is consistent:
| Symptom | Tool | What you look for |
|---|---|---|
| Memory climbs over time | Memory Profiler + LeakCanary | Rising baseline; retained Activities; the reference chain to the root |
| Stutter / dropped frames | CPU Profiler (System Trace) | Main-thread work or GC events lined up with the jank |
| App freezes (ANR in field) | Android Vitals -> CPU Profiler | Blocking work on the UI thread to move off-thread |
| Slow / repeated requests | Network Profiler | Duplicate calls (the N+1 pattern), oversized payloads |
| Killed in background a lot | Android Vitals (LMK) + Memory Profiler | High memory pressure; allocation churn to reduce |
Key Insight:
The durable takeaway: the earlier chapters tell you why leaks, jank, and memory pressure happen; these tools tell you where. Reproduce the symptom, capture the right artifact (heap dump, system trace, Vitals metric), follow it to the line of code, fix it, then re-measure to confirm — a longer time-between-GCs, no retained Activity, no dropped frames.