| name | perfetto-workflow-android-heap-dump |
| description | Use when the user has an Android trace containing a Java heap graph (heap dump) and wants to investigate memory usage, find leaks, or understand what is retaining memory. Walks through a guided workflow built on the heap_graph_* tables and the android.memory.heap_graph stdlib. |
Investigating an Android Java heap dump
This skill is the recommended first pass when the user asks "what's wrong
with this heap dump?", "why is this process using so much memory?", or
"what's leaking?". It assumes the trace was recorded with the ART perfetto
data source and contains at least one Java heap graph.
If the user has not yet loaded a trace into trace_processor, follow
the perfetto_infra_querying_traces skill first, then come back here.
Recording-side reference for heap dumps:
https://perfetto.dev/docs/data-sources/java-heap-profiler.
All queries below use $upid and $ts as placeholders. Substitute the
values you picked in step 1 — trace_processor does not interpret $
variables.
Mental model
A heap dump is a snapshot of every Java object alive in a process at a
moment in time, plus the references between them. To find a leak you
generally want to answer two questions:
- What is taking up the most memory? — sum object sizes by class or
by retained (dominated) size, not just by
self_size. Retained size
is the memory that would be freed if the object went away.
- Why isn't it getting GC'd? — walk the dominator tree (or the
shortest path from a GC root) to find the chain of references keeping
it alive.
The Perfetto stdlib has prebuilt views for both. Reach for those before
joining heap_graph_object and heap_graph_reference by hand. Browse the
modules under android.memory.heap_graph.* in the stdlib reference:
https://perfetto.dev/docs/analysis/stdlib-docs.
Step 1 — Confirm the trace has a heap graph and orient
INCLUDE PERFETTO MODULE android.memory.heap_graph.heap_graph_stats;
SELECT
upid,
graph_sample_ts,
total_heap_size,
reachable_heap_size,
total_obj_count,
reachable_obj_count,
anon_rss_and_swap_size,
oom_score_adj
FROM android_heap_graph_stats
ORDER BY graph_sample_ts;
Sanity checks at this point:
- Did any rows come back? If not, the trace doesn't contain a heap graph
and you should stop and tell the user.
- Is
reachable_heap_size close to total_heap_size? A big gap means a
lot of memory is unreachable but not yet collected — interesting on its
own but not the typical "leak" pattern.
- How does
total_heap_size compare to anon_rss_and_swap_size? If RSS
is much larger than the Java heap, the bloat is probably not Java —
consider native allocations instead.
- Pick the
upid and graph_sample_ts you'll focus on for the rest of
the investigation. If there are multiple dumps, the earliest one
usually shows the steady state; later dumps show growth.
Join with process to turn upid into a process name when reporting back
to the user — never expose raw upid values.
Step 2 — Find the classes dominating retained size
The android_heap_graph_class_summary_tree view aggregates the heap by
the shortest-path tree from GC roots, grouped by class name. It's the
fastest way to spot "this one class is holding most of the heap":
INCLUDE PERFETTO MODULE android.memory.heap_graph.class_summary_tree;
SELECT
name AS class_name,
root_type,
self_count,
self_size,
cumulative_count,
cumulative_size
FROM android_heap_graph_class_summary_tree
WHERE upid = $upid AND graph_sample_ts = $ts
ORDER BY cumulative_size DESC
LIMIT 30;
cumulative_size is the size of all objects of this class plus everything
they retain — this is what to sort by to find the dominant retainer.
self_size only counts the objects of the class itself.
Patterns that are usually worth flagging to the user:
- A single class with a
cumulative_size that's a large fraction of
reachable_heap_size.
- An unexpectedly large
self_count for a class that "should" only have
a handful of instances (Activities, Fragments, application singletons).
- A
root_type of ROOT_JAVA_FRAME or ROOT_JNI_GLOBAL retaining a lot
— these often indicate a native or JNI leak.
Step 3 — Use the dominator tree to find the retention chain
Once a suspicious class is identified, the dominator tree tells you what
is uniquely keeping its instances alive. An object's immediate
dominator is the closest ancestor that every path from a GC root must
pass through.
INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;
WITH suspect_objects AS (
SELECT o.id
FROM heap_graph_object o
JOIN heap_graph_class c ON o.type_id = c.id
WHERE o.upid = $upid
AND o.graph_sample_ts = $ts
AND c.name = 'com.example.LeakySingleton'
)
SELECT
d.id,
d.idom_id,
d.dominated_obj_count,
d.dominated_size_bytes,
d.depth
FROM heap_graph_dominator_tree d
JOIN suspect_objects s USING (id)
ORDER BY dominated_size_bytes DESC;
To walk up from a leaked object to its GC root, follow idom_id
recursively (it's NULL once you hit the super-root). Join each step
through heap_graph_object.type_id → heap_graph_class.name to get a
human-readable retention path.
For a per-class rollup of the dominator tree (often easier to read than
per-object), use android.memory.heap_graph.dominator_class_tree
instead.
Step 4 — Inspect specific references when needed
The dominator tree shows one retaining edge per object (the dominator).
If you need the full set of incoming/outgoing references — e.g. to
explain why an object can't be collected even though the dominator looks
benign — query heap_graph_reference directly. Note that
heap_graph_reference.owner_id is the source object id; you do not need
to join through reference_set_id.
SELECT
r.field_name,
r.field_type_name,
oc.name AS owner_class,
r.owned_id AS referent_id,
rc.name AS referent_class,
ro.self_size AS referent_self_size
FROM heap_graph_reference r
JOIN heap_graph_object oo ON r.owner_id = oo.id
JOIN heap_graph_class oc ON oo.type_id = oc.id
LEFT JOIN heap_graph_object ro ON r.owned_id = ro.id
LEFT JOIN heap_graph_class rc ON ro.type_id = rc.id
WHERE r.owner_id = $object_id;
Common things to look for here:
- An
ArrayList / HashMap / ConcurrentHashMap whose internal array
has thousands of slots — the leaked container is the suspect.
- A
WeakReference or SoftReference chain that turns out not to be
weak in practice (a strong reference is also held).
- An inner class (
Outer$Inner) with a this$0 field pinning an
Activity or Fragment — classic Android leak.
What to report back
A good summary for the user contains:
- The process name and the timestamp of the heap dump.
- Total reachable heap size and the top retainers by
cumulative_size,
with class names, not raw IDs.
- For the dominant retainer, the chain from a GC root to one example
object, expressed as
Class -> field -> Class -> field -> ….
- A hypothesis stated as a hypothesis, not a fact ("this looks like a
leaked Activity retained via the inner Listener; the next step is to
confirm by …").
Always keep the underlying SQL and object IDs available so the user can
audit. Do not make claims about what the code is doing without inspecting
the references.
Reference