| # Heap Dump Explorer |
| |
| The Heap Dump Explorer is a page in the Perfetto UI for analyzing Android |
| Java heap dumps. For every reachable object it shows the class, the |
| shallow and retained sizes, and the reference path from a GC root — so |
| you can answer what is in the heap, what is keeping each object alive, |
| and how much memory each one retains. |
| |
| This guide covers: |
| |
| - [Heap dumps vs. heap profiles](#heap-dumps-vs-heap-profiles) and when |
| to use which. |
| - [Capturing a heap dump](#capturing-a-heap-dump), both the lightweight |
| Perfetto heap graph and the fuller ART HPROF formats. |
| - How to use each tab of the explorer, starting with |
| [inspecting a single object](#inspecting-a-single-object) — the view |
| most investigations end up at. |
| - Worked [case studies](#case-studies): a leaked `Activity` and |
| duplicate bitmaps. |
| |
| ## Heap dumps vs. heap profiles |
| |
| <!-- TODO(zezeozue): Move this explanation into the memory guide |
| (docs/case-studies/memory.md or docs/getting-started/memory-profiling.md) |
| and cross-link from here instead of duplicating. --> |
| |
| - A **Java heap profile** samples _allocations over time_ as a |
| flamegraph of call stacks. It answers which code paths are |
| allocating memory while the trace is recorded. See the |
| [Java heap sampler](/docs/data-sources/native-heap-profiler.md#java-heap-sampling). |
| |
| - A **Java heap dump** is a _snapshot of the heap at one point in time_. |
| It captures every reachable object, the references between them, GC |
| roots and — depending on the format — field values, strings, |
| primitive array bytes and bitmap pixel buffers. |
| |
| The Heap Dump Explorer is for dumps. Use a heap profile instead for |
| allocation call-path analysis. |
| |
| ### What heap dumps are good for |
| |
| - **Memory leaks.** An object is reachable that shouldn't be. The |
| reference path from a GC root points at the holder — typically a |
| static field, a cached listener, or a `Handler` posting to a |
| destroyed context. |
| - **Retention surprises.** An object is small itself but retains many |
| megabytes through its references. The dominator tree and the |
| _Immediately dominated objects_ section show exactly what it is |
| holding on to. |
| - **Duplicate content.** Multiple copies of the same bitmap, string or |
| primitive array. The Overview groups them by content hash and shows |
| the wasted bytes. |
| - **Bitmap accounting.** Which bitmaps are alive, how large they are |
| and what is holding them. |
| - **Class breakdowns.** Which classes own the largest share of |
| retained memory. |
| |
| ### What heap dumps are not good for |
| |
| - **Allocation call paths.** A heap dump is a snapshot, not a |
| recording — it doesn't tell you _which code_ allocated an object. |
| Use a [Java heap profile](/docs/data-sources/native-heap-profiler.md#java-heap-sampling) |
| for that. |
| - **Native-only memory.** The dump covers the Java heap. For native |
| allocations use the |
| [native heap profiler](/docs/data-sources/native-heap-profiler.md). |
| - **Timing and performance.** Heap dumps say nothing about when |
| objects were created or how long operations took. |
| |
| ## Capturing a heap dump |
| |
| Two formats are supported. |
| |
| ### Perfetto heap graph (lightweight) |
| |
| Captures the object graph — classes, references, sizes, GC roots — but |
| not field values, strings, primitive array bytes or bitmap pixels. |
| Enough for retention, dominator and class-breakdown analysis. |
| |
| **Pros:** |
| |
| - Privacy-safe — no string values, pixel buffers or field contents |
| leave the device, so it can be captured from real users in the field |
| without leaking sensitive data. |
| - Does not require a `debuggable` process. |
| - Integrates with the rest of the Perfetto tooling: you can capture a |
| heap graph alongside heap profiles, memory counters and other data |
| sources in a single trace. |
| |
| **Cons:** |
| |
| - No content-based analysis — the Strings, Arrays and Bitmaps tabs and |
| the duplicate-content detection on the Overview are unavailable. |
| |
| Choose this format for leak investigations, dominator analysis and |
| class breakdowns, especially when capturing from non-debuggable |
| production builds. |
| |
| ```bash |
| $ tools/java_heap_dump -n com.example.app -o heap.pftrace |
| |
| Dumping Java Heap. |
| Wrote profile to heap.pftrace |
| ``` |
| |
| Use `--wait-for-oom` to trigger on `OutOfMemoryError`, or |
| `-c <interval_ms>` for continuous dumps. See |
| [Java heap dumps](/docs/data-sources/java-heap-profiler.md) for the |
| full config and |
| [OutOfMemoryError heap dumps](/docs/case-studies/android-outofmemoryerror.md) |
| for the OOM-triggered variant. |
| |
| ### ART HPROF (full detail) |
| |
| Everything the heap graph has, plus field values, primitive array |
| contents, string values and bitmap pixel buffers. Required for the |
| Strings, Arrays and Bitmaps tabs and for the duplicate-content |
| detection on the Overview tab. |
| |
| **Pros:** |
| |
| - Full visibility — field values, string contents, bitmap pixels and |
| primitive array bytes are all available. |
| - Enables duplicate-content detection and the Bitmaps gallery. |
| - The HPROF format is also understood by other tools such as Android |
| Studio. |
| |
| **Cons:** |
| |
| - Much slower to capture and freezes the target process for several |
| seconds (Perfetto works on a forked copy so the main process is |
| unaffected). |
| - Produces larger files. |
| - Contains the full contents of the heap, so it is not suitable for |
| capturing from real users — it will contain any sensitive data in |
| memory. |
| - Requires a `debuggable` process. |
| |
| Choose this format when you need content-level detail: hunting |
| duplicate bitmaps, inspecting string values, or exporting to other |
| tools. |
| |
| ```bash |
| $ adb shell am dumpheap -g -b png com.example.app /data/local/tmp/heap.hprof |
| $ adb pull /data/local/tmp/heap.hprof |
| |
| File: /data/local/tmp/heap.hprof |
| ``` |
| |
| `-b` encodes bitmap pixel buffers as the given format (`png`, `jpg`, |
| or `webp`) and is required for the Bitmaps gallery to render pixels. |
| `-g` forces a GC before the dump, so unreachable instances don't |
| appear in the result — use it when hunting a suspected leak. The |
| target process must be `debuggable` (a `userdebug`/`eng` build, or an |
| APK with `android:debuggable="true"`). |
| |
| NOTE: Sections marked _requires HPROF_ below are hidden on traces |
| captured with the heap graph format. |
| |
| Open the resulting trace by dragging it onto |
| [ui.perfetto.dev](https://ui.perfetto.dev) or clicking |
| _"Open trace file"_ in the sidebar. |
| |
| ## Opening the explorer |
| |
| There are two entry points: |
| |
| 1. **Sidebar.** Click _"Heapdump Explorer"_ under the current trace. |
| The entry only appears when the trace contains a heap dump. |
| |
|  |
| |
| 2. **From a heap graph flamegraph.** Click a diamond in a |
| _"Heap Profile"_ track to open the heap graph flamegraph, click a |
| node to select it, then click the menu icon in the node's details |
| popup and pick _"Open in Heapdump Explorer"_. This is covered in |
| detail under [Jumping from a flamegraph](#jumping-from-a-flamegraph). |
| |
|  |
| |
| The explorer is organized as tabs across the top. _Overview_, |
| _Classes_, _Objects_, _Dominators_, _Bitmaps_, _Strings_ and _Arrays_ |
| are fixed. Tabs you open by drilling into a specific object or |
| flamegraph selection are appended on the right and can be closed. |
| |
|  |
| |
| All tabs share the underlying `heap_graph_*` tables. Blue links — a |
| class name, an object id, a _Copies_ count — navigate to the |
| corresponding tab pre-filtered. |
| |
| ## Overview |
| |
| NOTE: The duplicate sections _require HPROF_. |
| |
| The Overview is the default landing page and summarizes the dump: |
| |
| - **General information.** Reachable instance count and the list of |
| heaps in the dump (typically `app`, `zygote`, `image`). |
| - **Bytes retained by heap.** Java, native and total sizes per heap, |
| with a total row at the top. Use this to see whether the problem |
| is on the Java heap, in native memory, or both. |
| - **Duplicate bitmaps / strings / primitive arrays.** Duplicated |
| content grouped by content hash. Each row shows the copy count |
| and the wasted bytes; clicking _Copies_ opens the relevant tab |
| filtered to that group. |
| |
|  |
| |
| ## Classes |
| |
| The Classes tab lists every class in the dump, sorted by _Retained_ |
| descending: |
| |
| - **Count** — reachable instances. |
| - **Shallow / Shallow Native** — combined self-size of all instances. |
| - **Retained / Retained Native** — bytes freed if every instance |
| became unreachable. |
| - **Retained #** — the number of objects that would go with them. |
| |
| ![Classes tab sorted by Retained; `byte[]` and `java.lang.String` at the top, `com.heapleak.ProfileActivity` further down with Count 1.](../images/heap_docs/05-classes.png) |
| |
| Use this tab when you have a suspect class, or want a top-down view |
| of which classes own the most memory. Clicking a class name opens |
| Objects filtered to that class. |
| |
| ## Objects |
| |
| The Objects tab lists reachable instances. Opening it from Classes or |
| from a duplicate group applies the filter automatically; opening it |
| directly shows every object. |
| |
| Each row has the object identifier (short class name + hex id), its |
| class, shallow and retained size, and its heap. `java.lang.String` |
| rows carry a badge with a preview of the value, so strings can be |
| scanned at a glance. |
| |
|  |
| |
| Clicking an object opens its [object tab](#inspecting-a-single-object). |
| Typical uses: identifying a stale `Activity` after a leak, or the |
| instance of a data class holding the largest subgraph. |
| |
| ## Inspecting a single object |
| |
| **The _Shortest Path from GC Root_, _Dominator Tree Path_ and _Objects |
| with References to this Object_ are the key sections for most |
| investigations.** The shortest path shows the fewest reference hops |
| keeping the object alive; the dominator tree path shows the chain of |
| objects that exclusively retain it; the reverse references list every |
| object holding a field pointer to it. |
| |
| Clicking any object in any tab opens a closable tab for that instance. |
| Multiple object tabs can be open at once. |
| |
| The object tab contains everything known about the instance: |
| |
| - **Header** with the object id, plus an _Open in Classes_ shortcut |
| when the object is itself a `Class`. |
| - **Bitmap preview** for bitmap instances, with a download button. |
| - **Shortest Path from GC Root** — the shortest chain of references |
| from a GC root to this object. |
| - **Dominator Tree Path** — the chain of dominators keeping this |
| object alive, one step per row with the holder and the field name. |
| - **Object info** — class, heap, root type. |
| - **Object size** — shallow, retained and reachable sizes split by |
| Java / native / count. |
| - **Class hierarchy** — the full inheritance chain up to |
| `java.lang.Object`, plus the instance size for class objects. |
| Clicking any class opens **Classes** filtered to that class and its |
| subclasses. |
| - **Static fields** (for class objects), **instance fields** (for |
| ordinary objects) or **array elements** (for arrays). Reference |
| values are clickable and jump to the referenced object. For byte |
| arrays, _Download bytes_ exports the raw data. |
| - **Objects with references to this object** — the reverse references. |
| Every instance that has a field pointing at this one. |
| - **Immediately dominated objects** — what would be freed if this |
| instance became unreachable. |
| |
| ![Object tab (top) for `ProfileActivity 0x0004f1ae`: Sample Path from GC Root goes `Class<ProfileActivity> → com.heapleak.ProfileActivity.history → ArrayList → Object[0] → ProfileActivity`; retained 117.6 KiB across 1,604 objects.](../images/heap_docs/12-object-tab-top.png) |
| |
|  |
| |
| Both sections auto-collapse on large objects — click the header to |
| expand. |
| |
| ## Dominators |
| |
| The Dominators tab shows the |
| [dominator tree](https://en.wikipedia.org/wiki/Dominator_(graph_theory)) |
| of the heap. In a directed graph, node `a` _dominates_ node `b` when |
| every path from a root to `b` must pass through `a`. Applied to a heap: |
| if you free `a`, everything it dominates — every object reachable |
| _only_ through `a` — is also freed. The dominator tree groups the heap |
| into these "freed-together" subtrees, making it easy to see which |
| single objects gate the largest chunks of retained memory. |
| |
|  |
| |
| _Root Type_ (e.g. `THREAD`, `STATIC`, `JNI_GLOBAL`) identifies how each |
| dominator is itself kept alive. Click a row to open its object tab and |
| walk the reference path. |
| |
| Use this tab when there is no specific suspect and the question is |
| simply where the memory has gone. |
| |
| ## Bitmaps |
| |
| NOTE: Pixel previews and duplicate detection _require HPROF_. |
| |
| The Bitmaps tab is a gallery of every `android.graphics.Bitmap` in the |
| dump. With an HPROF, each bitmap's pixels are rendered inline. |
| |
|  |
| |
| Each card shows the rendered pixels, dimensions (px and dp), DPI, |
| retained memory and a _Details_ button that opens the object tab. |
| Pixel buffers may be RGBA, PNG, JPEG or WebP depending on how they |
| were stored. |
| |
| The path dropdown above the gallery picks which reference path to |
| overlay on each card: _Shortest path_ (fewest edges from a GC root), |
| _Dominator path_ (the chain of dominators), or _No path_. Showing a |
| path is the fastest way to spot an `Activity`, `Fragment` or |
| `Handler` holding leaked bitmaps. |
| |
|  |
| |
| Two tables at the bottom list bitmaps with and without pixel data, |
| with filter, sort and export controls. Arriving via _Copies_ on |
| Overview pre-filters the tab by buffer content hash, leaving only the |
| visually identical bitmaps in that group. |
| |
| ## Strings |
| |
| NOTE: The Strings tab _requires HPROF_. |
| |
| The Strings tab lists every `java.lang.String` with its value. The |
| summary card reports the total number of strings, the number of |
| distinct values and the total retained memory. The gap between total |
| and distinct is memory spent on duplicates. |
| |
|  |
| |
| Filter by value to find data that was expected to be unique: a user |
| id, a serialized config payload, an error message repeated thousands |
| of times. Clicking a row opens its object tab, where the |
| reverse-references section lists every object holding that string. |
| |
| ## Arrays |
| |
| NOTE: The Arrays tab _requires HPROF_. |
| |
| The Arrays tab lists primitive arrays (`byte[]`, `int[]`, `long[]`, |
| ...) together with a stable content hash. Filtering by _Content Hash_ |
| returns every array with the same bytes; this is how the Overview |
| detects duplicate arrays. |
| |
|  |
| |
| Two common uses: finding a large duplicated `byte[]` that backs an |
| image or serialized buffer, and jumping from a container object to |
| the primitive array holding its data. |
| |
| ## Jumping from a flamegraph |
| |
| The heap graph flamegraph has an _Open in Heapdump Explorer_ action |
| that opens the explorer on the list of objects matching a selected |
| allocation path. Use it to inspect a flamegraph node object-by-object: |
| |
| 1. Click a diamond in a _"Heap Profile"_ track to open the flamegraph. |
| |
|  |
| |
| 2. Click a node to select it, then click the menu icon in the node's |
| details popup. Pick _"Open in Heapdump Explorer"_. |
| |
|  |
| |
| This opens a new closable _Flamegraph Objects_ tab listing every |
| object allocated along the selected path. Dominator flamegraph |
| nodes produce a dominator-based selection; regular nodes produce |
| a path-based selection. |
| |
|  |
| |
| 3. From there, click any object to open its |
| [object tab](#inspecting-a-single-object), or use _Back to Timeline_ |
| to return to the flamegraph view. |
| |
| Multiple flamegraph selections can be open at once, each as its own |
| tab — useful for comparing two call stacks side by side. |
| |
| ## Case studies |
| |
| <!-- TODO(zezeozue): Break these case studies out and integrate them into |
| the existing memory guides (docs/case-studies/memory.md). Rationalize |
| the material so it isn't duplicated across docs. --> |
| |
| ### Finding a leaked Activity |
| |
| A developer on a Kotlin app reports that rotating their profile |
| screen a few times drives the Java heap upward and never comes back |
| down. The screen is unremarkable — an `Activity`, a view hierarchy, |
| one avatar — and rotating _should_ destroy the old instance. It |
| doesn't. |
| |
| A quick grep turns up a "breadcrumb" list the team added a while |
| ago for crash reporting. It stores every `ProfileActivity` instance |
| created, and is never cleared: |
| |
| ```kotlin |
| class ProfileActivity : Activity() { |
| companion object { |
| val history = mutableListOf<ProfileActivity>() // never cleared |
| } |
| |
| override fun onCreate(state: Bundle?) { |
| super.onCreate(state) |
| setContentView(R.layout.profile) |
| history += this // <-- the bug |
| } |
| } |
| ``` |
| |
| The intent was to keep a lightweight trail of recent screens for |
| crash reports. What it actually does is pin every `ProfileActivity` |
| ever created: `onDestroy` runs on the old one, but the class's |
| static `history` list keeps a strong reference — along with the old |
| Activity's entire view hierarchy. |
| |
| **Capturing.** The heap graph format is enough to chase an Activity |
| leak; it carries the full object graph and GC roots: |
| |
| ```bash |
| $ tools/java_heap_dump -n com.example.app -o /tmp/profile.pftrace |
| |
| Dumping Java Heap. |
| Wrote profile to /tmp/profile.pftrace |
| ``` |
| |
| Rotate the device a handful of times first so multiple instances |
| accumulate. Drag the file onto |
| [ui.perfetto.dev](https://ui.perfetto.dev) and click _Heapdump |
| Explorer_ in the sidebar. |
| |
| **Confirming the leak.** Open **Classes** and find |
| `com.heapleak.ProfileActivity`. `Count` should be 0 after the user |
| has navigated away; here it's 5, one per rotation: |
| |
|  |
| |
| Clicking the class name opens **Objects** filtered to |
| `ProfileActivity`. Every row is one live instance: |
| |
|  |
| |
| **Reading the reference path.** Click the top row to open its object |
| tab. The _Sample Path from GC Root_ is the chain of field references |
| keeping this instance alive: |
| |
| ![Object tab for a leaked ProfileActivity. Sample Path from GC Root: Class<ProfileActivity> → com.heapleak.ProfileActivity.history → ArrayList.elementData → Object[0] → ProfileActivity. Retained 117.6 KiB, ~1,600 reachable objects.](../images/heap_docs/12-object-tab-top.png) |
| |
| Read bottom-up: the runtime keeps the `java.lang.Class<ProfileActivity>` |
| alive (as it does for every loaded class); that class has a |
| companion-object field `history`; that field points at an `ArrayList` |
| whose element 0 is this `ProfileActivity`. The hop from the class |
| object to `history` names the bug — a static list of Activities. |
| |
| The _Object Size_ block quantifies the cost: one leaked Activity is |
| pinning 117.6 KiB and ~1,600 reachable objects. Multiply by |
| five (the `Count`) and the leak is already ~600 KiB of Activity |
| graphs sitting in the heap. Further down the same tab are the |
| _Objects with References to this Object_ and _Immediately Dominated |
| Objects_ sections: |
| |
|  |
| |
| Expanding _Immediately Dominated Objects_ shows everything going |
| down with the leak — the `Activity`'s view hierarchy and the rest |
| of the state it transitively retains. None of it is supposed to |
| outlive the Activity; all of it does, because one companion-object |
| list is holding the root. |
| |
| **Fix.** Never store an `Activity` in a `static` or companion-object |
| container. If you want a breadcrumb trail for crash reports, store |
| strings with a bounded capacity instead: |
| |
| ```kotlin |
| object Breadcrumbs { |
| private const val CAPACITY = 16 |
| private val trail = ArrayDeque<String>(CAPACITY) |
| |
| @Synchronized |
| fun record(event: String) { |
| while (trail.size >= CAPACITY) trail.removeFirst() |
| trail.addLast("${System.currentTimeMillis()} $event") |
| } |
| } |
| |
| class ProfileActivity : Activity() { |
| override fun onCreate(state: Bundle?) { |
| super.onCreate(state) |
| setContentView(R.layout.profile) |
| Breadcrumbs.record("ProfileActivity.onCreate") |
| } |
| } |
| ``` |
| |
| Re-run the same repro and re-dump. The Classes tab now shows |
| exactly one `ProfileActivity` — the currently visible screen — |
| instead of one per rotation. |
| |
| This tiny demo saves ~1.5 MiB of app heap; a real screen with a |
| live view hierarchy sees the difference in tens of megabytes. Any |
| `Activity` subclass showing `Count > 0` in a dump captured after |
| the user navigated away is a leak. |
| |
| The same recipe finds the other common shapes of Activity leak — |
| delayed-message `Handler`s, unregistered listeners, coroutines that |
| outlived their scope. The last hop before the Activity in the |
| reference path always names the holder; the fix is to clear that |
| field at the right lifecycle callback. |
| |
| ### Tracking down duplicate bitmaps |
| |
| A Kotlin feed app is running out of memory on long scrolls. `dumpsys |
| meminfo com.example.feed` reports a `Graphics:` line several times |
| bigger than the pixels actually on screen, and the in-app image |
| cache looks small. Something else is holding pixels. |
| |
| The suspect turns out to be a `RecyclerView` adapter that decodes |
| each row's thumbnail from resources on every bind, and appends the |
| result to a companion-object list: |
| |
| ```kotlin |
| class FeedAdapter(private val res: Resources) : RecyclerView.Adapter<VH>() { |
| companion object { |
| val cache = mutableListOf<Bitmap>() // grows without bound |
| } |
| |
| override fun onBindViewHolder(holder: VH, position: Int) { |
| val bmp = BitmapFactory.decodeResource(res, R.drawable.thumb) |
| cache += bmp // "cache" — actually just accumulates |
| holder.image.setImageBitmap(bmp) |
| } |
| // ... |
| } |
| ``` |
| |
| Every bind decodes a fresh copy of the same PNG. Every copy is then |
| held forever by `cache`. The pixels all hash to the same value, but |
| they're different `Bitmap` instances with different backing |
| `byte[]`s. |
| |
| **Capturing.** Duplicate detection needs the hash of each bitmap's |
| pixel buffer, which only the HPROF format carries. `-b png` encodes |
| the pixels so the Bitmaps gallery can render previews: |
| |
| ```bash |
| $ adb shell am dumpheap -g -b png com.example.feed /data/local/tmp/feed.hprof |
| $ adb pull /data/local/tmp/feed.hprof |
| ``` |
| |
| Scroll the feed long enough to reproduce the bloat before dumping — |
| the adapter's `cache` only grows on bind. |
| |
| **Triage on the Overview.** The Overview groups bitmaps by |
| pixel-buffer hash. Each row shows copy count, total bytes across |
| all copies, and wasted bytes — what deduplicating to a single copy |
| would save: |
| |
|  |
| |
| The row shows what was accumulated: twelve copies of one 128×128 |
| asset, all with the same content hash. The _Duplicate Strings_ and |
| _Duplicate Primitive Arrays_ cards below work the same way — same |
| grouping, same sizing — and are useful when the wasted memory is in |
| text (e.g. a config payload duplicated thousands of times) or |
| primitive buffers. All three duplicate detectors require HPROF |
| because they hash the actual content, which the heap graph format |
| doesn't carry. |
| |
| **Drill into the copies.** Click _Copies_ on that row. **Bitmaps** |
| opens pre-filtered to that content-hash group, so only those copies |
| render as cards: |
| |
|  |
| |
| **Find the holder.** Set the path dropdown to _Shortest path_. The |
| reference chain below each card is the fields keeping that bitmap |
| alive: |
| |
|  |
| |
| Every chain in the gallery is identical: `Class<FeedAdapter>.cache → |
| ArrayList → Bitmap`. All twelve copies share one holder — a |
| cache-layer bug, one field to fix. |
| |
| The shape of the chains is the diagnostic. Two other patterns to |
| recognize on future investigations: |
| |
| - _Each copy has a different chain_ → call-site bug. There's no |
| cache, or callers are bypassing it. |
| - _The chain passes through an `Activity`_ → fix the Activity leak |
| first ([previous case study](#finding-a-leaked-activity)); the |
| bitmaps will follow. |
| |
| **Fix.** There's no real reason to keep a side list of `Bitmap`s at |
| all — Android already has a `LruCache<K, Bitmap>`, scoped to the |
| application, with eviction you control: |
| |
| ```kotlin |
| class FeedAdapter(private val res: Resources) : RecyclerView.Adapter<VH>() { |
| companion object { |
| private val cache = object : LruCache<Int, Bitmap>(4) { |
| override fun sizeOf(key: Int, value: Bitmap) = 1 |
| } |
| } |
| |
| override fun onBindViewHolder(holder: VH, position: Int) { |
| val key = R.drawable.thumb |
| val bmp = cache[key] ?: BitmapFactory.decodeResource(res, key).also { cache.put(key, it) } |
| holder.image.setImageBitmap(bmp) |
| } |
| // ... |
| } |
| ``` |
| |
| **Verify.** Scroll the feed the same distance, re-dump, re-open. |
| The Overview should declare `No duplicate bitmaps found`, and the |
| app-heap retained bytes should drop accordingly: |
| |
|  |
| |
| The _wasted bytes_ total across all groups on the Overview is the |
| cleanest single-number scorecard — watching it drop from dump to |
| dump is how you confirm each fix and catch regressions. |
| |
| ## See also |
| |
| - [Java heap dumps](/docs/data-sources/java-heap-profiler.md) — |
| recording config, troubleshooting and SQL schema reference. |
| - [Memory case study](/docs/case-studies/memory.md) — end-to-end guide |
| to investigating Android memory issues, covering `dumpsys meminfo`, |
| native heap profiles and Java heap dumps together. |
| - [OutOfMemoryError heap dumps](/docs/case-studies/android-outofmemoryerror.md) |
| — capturing a heap dump automatically on OOM. |
| - [Native heap profiler](/docs/data-sources/native-heap-profiler.md) — |
| for allocation call-path analysis rather than heap contents. |