Heap Dump Explorer

The Heap Dump Explorer is a page in the Perfetto UI for analyzing Android ART 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

The Heap Dump Explorer is for dumps. Use an ART allocation profile instead for allocation call-path analysis.

What heap dumps are good for

What heap dumps are not good for

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:

Cons:

Choose this format for leak investigations, dominator analysis and class breakdowns, especially when capturing from non-debuggable production builds.

$ 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 ART heap dumps for the full config and OutOfMemoryError heap dumps 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:

Cons:

Choose this format when you need content-level detail: hunting duplicate bitmaps, inspecting string values, or exporting to other tools.

$ 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 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.

    Perfetto UI with a heap dump loaded; the sidebar shows "Heapdump Explorer" under "Current Trace".

  2. From a heap graph flamegraph. Click a diamond in an "ART heap dump" track to open the heap graph flamegraph, click a node to select it, then open the Drill down menu in the node's details popup and pick "Open in Heapdump Explorer". This is covered in detail under Jumping from a flamegraph.

    Heap graph flamegraph with the `java.lang.String` node selected; the details popup lists its Cumulative size, Root Type and Self Count, and its overflow menu is open with "Open in Heapdump Explorer" visible.

The explorer is organized as tabs across the top. Overview, Flamegraph, Classes, Objects, Dominators, Bitmaps, Strings, Arrays and Callstack are fixed (Callstack shows the allocation stack that triggered the dump, and only has data for dumps captured on OutOfMemoryError on recent Android versions). Tabs you open by drilling into a specific object or flamegraph selection are appended on the right and can be closed.

Tab bar with the seven fixed tabs and a dynamic object tab opened for `ProfileActivity 0x00032f52`.

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:

Overview tab: General Information (437,681 reachable instances across app/image/zygote heaps), Bytes Retained by Heap (24.4 MiB total, 1.5 MiB on the app heap), and a Duplicate Bitmaps group wasting 785.8 KiB across 12 copies of the same 128×128 image.

For a dump captured on OutOfMemoryError, the Out of Memory Error card sits below Bytes Retained by Heap:

Overview tab for an OOM dump. Below Bytes Retained by Heap, the Out of Memory Error card reports Allocation size 10.00 MB, Free until OOM 8.91 MB, and the raw ART error message.

Flamegraph

The Flamegraph tab shows the whole heap at once, aggregated by class. Where Classes and Objects answer "how much memory does class X own", the flamegraph also shows where in the reference graph that memory sits: which chains of references, starting at the GC roots, lead to it. It is usually the fastest way to spot that one subtree of the heap is disproportionately large.

The same flamegraph appears in the timeline when you click a heap dump diamond in an "ART heap dump" track — every feature below works identically there. A few extras that only exist in the timeline variant are covered at the end under the timeline flamegraph.

Timeline on top, heap graph flamegraph in the bottom panel after clicking the heap dump diamond on the process track.

How to read it

A heap is a graph — objects reference each other freely — but a flamegraph draws a tree, so the graph is first converted:

Reading top-down: the synthetic root row at the top spans the whole dump; each row below it is one more reference hop away from the GC roots. The width of a node is proportional to the selected metric (bytes or object count) in that node's entire subtree. Objects with no known class name show as unknown.

One caveat that trips people up: in this shortest-path tree, a node's subtree is not the same as its retained size. An object referenced from two places is drawn only under its shortest path, but it would survive the other reference being dropped. When you need "what would actually be freed", switch to the dominator metrics below.

Nodes too narrow to draw (less than ~3 pixels) are collapsed into a grey (merged) node. They are still counted in every total; zoom in or add a filter to see them individually.

Choosing a metric

The dropdown at the top-left switches what the flamegraph measures:

Metric Tree Node width is
Object Size Shortest-path Shallow bytes of all objects in the subtree
Object Count Shortest-path Number of objects in the subtree
Dominated Object Size Dominator Bytes freed if the subtree's top object died
Dominated Object Count Dominator Objects freed if the subtree's top object died

The two Dominated metrics build the tree from the dominator tree instead of shortest paths: each object hangs under the object that exclusively retains it. A node's cumulative value is therefore its true retained size — exactly what the garbage collector would reclaim if those objects became unreachable. Use Object Size to follow the actual reference structure of the heap, and Dominated Object Size to attribute memory to the objects responsible for keeping it alive.

Sizes count the Java shallow size of each object. Native memory registered against a Java object (for example bitmap pixel buffers on modern Android) is shown as a separate child node labelled [native] <ClassName> and is counted in all cumulative totals. Native memory that is not registered this way does not appear in the dump at all; use the native heap profiler for that.

Cumulative, Self and Self Count

Hover or click a node to see its numbers:

Flamegraph with `java.lang.String` selected. Its details popup lists Cumulative size (2.48 MiB, 10.48%), Root Type (`ROOT_INTERNED_STRING`), Heap Type and Self Count (53,546).

Because nodes are only merged when Root Type and Heap Type match, the same class can legitimately appear as several sibling nodes — e.g. one java.lang.String node per heap.

Top Down and Bottom Up

The radio buttons at the top-right flip the direction of aggregation:

Zooming

Double-click a node (or use Zoom in from its popup) to stretch it to the full width. Nothing is filtered out — ancestors stay visible, greyed, and totals don't change. Double-click the root row to zoom back out. Zooming is purely visual; to actually cut the data down, use filters.

Filters

The filter bar reshapes the tree. Type into it directly, or press the + button for a guided form. Active filters show as chips; double-click a chip to edit it, click its x to remove it, or use the bin button to clear everything.

Patterns are matched against the class name; bare text is a literal, case-insensitive substring match (String matches java.lang.String), while /.../ is a case-sensitive regular expression (^/$ anchor it exactly) and /.../i a case-insensitive one. Patterns also match against a node's Root Type and Heap Type values, so SS: ROOT_JNI_GLOBAL or SS: zygote work too.

There are four filter types plus Pivot. In the filter bar, prefix the pattern with the short or full name; with no prefix the text becomes a Show Stack filter. Multiple filters can be typed in one go, separated by spaces: SS: main HF: /alloc.*/.

Filters persist when you switch metrics or between Top Down and Bottom Up, and the copy button next to the bar copies the active filter set as text so it can be shared or pasted back.

Pivot

Pivoting (P: in the filter bar, or Pivot on this frame from a node's popup) re-roots the flamegraph at every node matching the pattern:

This is the "show me everything about this class in one picture" view: its total footprint, what it is made of, and who is holding it, without walking objects one at a time. A pivot shows as a Pivot: ... chip; only one can be active at a time (setting a new one replaces it), neither Top Down nor Bottom Up is selected while pivoted (picking one clears the pivot), and removing the chip returns to Top Down.

The object tab integrates with pivot directly: the Shortest Path from GC Root and Dominator Tree Path sections each have a View in Flamegraph button that opens this tab pivoted on that specific instance's path (the chip reads ClassName (this instance)), using the Object Size or Dominated Object Size metric respectively.

Node actions

Clicking a node opens its details popup with four menus, grouping everything you can do from a node:

Actions launched from a node match that node exactly (the pattern is anchored as ^name$), so filtering on java.lang.String will not accidentally match java.lang.StringBuilder.

The timeline flamegraph

The flamegraph in the timeline's "ART heap dump" details panel has two extras:

In both flamegraphs, if the heap graph in the trace is incomplete (the dump was cut short), a warning modal offers to show the import errors; the flamegraph still renders with whatever data arrived.

Classes

The Classes tab lists every class in the dump, sorted by Retained descending:

Classes tab sorted by Retained; `byte[]` and `java.lang.String` at the top, `com.heapleak.ProfileActivity` further down with Count 1.

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.

Objects tab filtered to `java.lang.String`; 106,474 instances of 437,681 total, sorted by retained bytes.

Clicking an object opens its object tab. 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: