What is deepheap?

deepheap is an MCP server that lets an AI client analyze Java (and other JVM languages) heap dumps, thread dumps, and GC logs via tool calls. You point your client at the deepheap binary, ask the client to load an artifact, and then ask questions in natural language: "what's holding the most memory?", "which threads are blocked?", "are GC pauses growing?", "where are the duplicate strings?"

A headless replacement for Eclipse MAT and VisualVM: no GUI, the AI client is the interface.

You need:

  • A JVM artifact to analyze: a HotSpot HPROF heap dump (from jmap, jcmd GC.heap_dump, or -XX:+HeapDumpOnOutOfMemoryError), a thread dump (jstack / JEP 425 JSON), or a GC log.
  • An MCP-capable AI client (Claude Code, Cursor, VS Code with the MCP extension, etc.)
  • JDK 25+ on the analyzer machine (see the FAQ)
  • A deepheap license file (required for heap-dump analysis; optional for thread-dump and GC log tools). See Downloads.

A typical session: connect → load dump → ask top-level questions → drill into specific objects or retention paths. Skip straight to First analysis if you'd rather see a worked example.

Why deepheap?

Where existing tools fall short for AI-augmented workflows, and how deepheap addresses the gaps.

The problem with Eclipse MAT and VisualVM

They excel at interactive GUI analysis but hit friction:

Why MCP over OQL?

OQL is powerful but requires the analyst to know what they're looking for. An MCP tool call lets the AI iterate: start with top-level class counts, narrow to suspect instances, trace a GC root path, without the analyst switching context or learning a query language. The AI drives the investigation; deepheap provides the facts.

Why deterministic engine + LLM interpretation?

deepheap does not feed raw heap bytes to the LLM. It computes retained sizes, resolves reference chains, and extracts structured facts using deterministic algorithms. The LLM reads those facts and synthesizes an explanation; it cannot invent object counts or retained sizes, because it does not see the raw dump. This matters especially for enterprise heap dumps that may contain user data, secrets, or prompt-injection payloads. (String content is redacted by default; use --show-strings to override when you control the dump.)

How it works

How data flows from a heap dump on disk to the AI client, and the two filters that sit in between.

Data flow

The dump never leaves your machine. The MCP client talks to a local deepheap server that queries the dump and returns structured results:

String-content gate

On by default. Every String, StringBuilder, StringBuffer, and Groovy GString value is replaced with a shape summary like String(length=42, format=json-object). Every byte[], char[], and short[], regardless of whether it backs a String, is redacted to byte[N] (REDACTED, format=uri). The format label is chosen by the server from a closed vocabulary of about 20 values (json-object, json-array, uri, jwt, pem, base64, xml, sql, uuid, iso-date, …). Because that label is only about four bits drawn from a fixed set, heap content can't carry natural-language instructions to the model no matter how much attacker-controlled text the underlying string holds. Disable with --show-strings only when you fully control and trust the dump.

Sensitive-name gate

On by default. A second pass over rendered object detail redacts field or map-key values whose name matches a credential pattern (password, token, secret, apiKey, credential, …). Disable with --show-secrets. The two gates are independent; you can enable one without the other.

Key concepts

Terms used on this page. Skim once, refer back as needed.

MCP
Model Context Protocol. A standard that lets AI clients call tools exposed by a local or remote server.
HPROF heap dump
A binary snapshot of every object in a JVM's heap, plus thread stacks and class metadata. File extension .hprof.
Shallow size
The memory used by an object itself: its header and fields, nothing more.
Retained size
The memory that would be freed if this object were garbage-collected: itself plus everything it exclusively keeps alive.
Dominator tree
X dominates Y if every path from a GC root to Y goes through X. The tree of all such relationships shows who really retains what.
GC root
An entry point the garbage collector starts from: static fields, thread stacks, JNI references, locked monitors.
ClassLoader leak
A discarded ClassLoader is still reachable, keeping all its classes and their static state alive. Common in OSGi, plugin systems, and hot-redeploy app servers.
Off-heap memory
Memory allocated outside the Java heap: DirectByteBuffer, native memory segments, mapped files. Counts against process RSS but not against the Java heap.

Quickstart

deepheap ships as a self-contained zip. No installer, no daemon. Your AI client launches the binary.

Prerequisites
  • JDK 25+  (Adoptium Temurin recommended)
  • macOS, Linux, or Windows
  • ~20 MB disk for the binary; heap dumps stay wherever they are
  • A deepheap license file (required for heap-dump analysis; optional for thread-dump and GC log tools). Obtained from the Downloads page.
  1. 1

    Request a license and download

    Request a trial license and grab deepheap-mcp-<version>.zip from the Downloads page. The license file is emailed to you and is required for the heap-dump tools; the thread-dump and GC log tools run without one.

  2. 2

    Unzip to a permanent location

    # macOS / Linux
    unzip deepheap-mcp-<version>.zip -d ~/deepheap
    
    # Windows (PowerShell)
    Expand-Archive deepheap-mcp-<version>.zip -DestinationPath $HOME\deepheap
  3. 3

    Verify the installation

    # macOS / Linux
    ~/deepheap/deepheap-<version>/bin/mcp --help
    
    # Windows (PowerShell)
    & "$HOME\deepheap\deepheap-<version>\bin\mcp.bat" --help

    Usage text should print.

  4. 4

    Connect your MCP client

    See MCP setup below for client-specific configuration.

  5. 5

    Load a dump and ask a question

    Restart your client so it picks up the new server, then jump to First analysis for a worked example.

Your first analysis

Once wired up, the AI does the work. You ask in natural language; it picks the right tools. A first session might look like this:

What you ask

  • Load the heap dump at /path/to/app.hprof.
  • Show me the top 20 classes by retained size.
  • Why is this com.example.SessionCache still alive?
  • Find duplicate strings wasting the most memory.
  • Are there any open file descriptors I should know about?

What runs under the hood

heap_dump__read↓heap_dump__get_classes (sort: retained)↓heap_dump__get_instances↓heap_dump__get_gc_root_path↓heap_dump__get_duplicate_strings↓heap_dump__get_open_files
Tip: the first call (heap_dump__read) parses the file and builds the dominator tree. Expect seconds on a small dump, several minutes on a multi-gigabyte one. Every subsequent call is fast because the parsed snapshot is cached.

MCP setup

deepheap speaks MCP over stdio (default) or Streamable HTTP. Pick your client:

Start with stdio mode: your client launches the server as a subprocess. Use HTTP for shared or remote deployments where multiple clients connect to one running server.

Windows: the launch command is bin\mcp.bat instead of bin/mcp. When registering a stdio server, wrap it in cmd /c, since batch files are run by the shell rather than executed directly. For example: claude mcp add --transport stdio deepheap -- cmd /c C:\deepheap\deepheap-<version>\bin\mcp.bat. The examples below show macOS/Linux paths; substitute your Windows path.

Add it with the CLI (recommended)

claude mcp add writes the config for you. Pick a scope:

# Just this project, just you (default)
claude mcp add --transport stdio deepheap -- /path/to/deepheap/bin/mcp

# All your projects, just you
claude mcp add --transport stdio --scope user deepheap -- /path/to/deepheap/bin/mcp

# Shared with your team (writes .mcp.json, commit it)
claude mcp add --transport stdio --scope project deepheap -- /path/to/deepheap/bin/mcp

Anything after -- is the launch command, so pass server flags there, for example -- /path/to/deepheap/bin/mcp --root=/path/to/dumps. Confirm it registered with claude mcp list or claude mcp get deepheap, and check live status from inside a session with /mcp (which manages and authenticates servers but does not add them).

Or edit .mcp.json directly

The team scope writes this file in your project root; you can also create it by hand:

// .mcp.json
{
  "mcpServers": {
    "deepheap": {
      "type": "stdio",
      "command": "/path/to/deepheap/bin/mcp"
    }
  }
}
Claude Code launches the server as a subprocess and relaunches it on the next tool call. After updating deepheap, restart Claude Code so the new build is loaded; the running server keeps the previous version until it's stopped.
Tip: deepheap can read any file your OS user can. Start it with --root=/path/to/dumps (a launch argument) to confine reads to your dump directory.

Common tasks

Task-oriented playbooks for the questions that come up most often. Open any card for the tool sequence and what to look for in the output. If you want a single end-to-end example first, see First analysis.

1
Find a memory leak
▼

Start with the big picture, then narrow down to the leaking objects and trace who's keeping them alive.

heap_dump__read→heap_dump__get_classes (sort: retained)→heap_dump__get_instances→heap_dump__get_gc_root_path
What to look for
  • Top retained classes that aren't framework baselines (e.g. char[], String). Application-specific caches, containers, or session objects high in this list are the first suspects.
  • A large gap between shallow and retained size on one class signals the entry point to a big subgraph.
  • One or two outsized instances of the same class, not millions of small ones. A single retained map with 20 GB underneath beats a swarm of tiny objects.
  • If the top of the list is uninformative, drill into the dominator tree node for the largest application object (see card 6).
A growing heap trend plus declining reclamation efficiency in a GC log (card 4) confirms it's a real leak, not a high-water snapshot.
2
Why is this object still alive?
▼

Given an object ID (from heap_dump__get_instances or heap_dump__get_object_detail), trace the exact path from a GC root to that object.

heap_dump__get_instances (find objectId)→heap_dump__get_gc_root_path(objectId)→inspect root type in chain
What to look for
  • java_frame root: the object is on a live thread's stack. Likely held by an in-flight request, a long-running task, or a thread that should have exited.
  • jni_global root: a native library is holding it. Common with JDBC drivers, JNI integrations, off-heap caches.
  • sticky_class or static field: kept alive by a ClassLoader. If the ClassLoader itself looks orphaned, jump to card 3.
  • A long retention chain through a HashMap, ConcurrentHashMap, or ThreadLocal is usually the actual leak surface.
3
Investigate a ClassLoader leak
▼

ClassLoader leaks are common in OSGi containers, application servers, and plugin systems. Each leaked loader prevents all its classes and their static state from being GC'd.

heap_dump__get_duplicate_classes→heap_dump__get_class_loaders→heap_dump__get_instances (leaking loader)→heap_dump__get_gc_root_path
What to look for
  • Multiple copies of the same class loaded by different loaders is the telltale sign of a redeploy or plugin leak.
  • ClassLoaders with surprisingly large retained sizes. The loader's retained size includes every class it loaded and all their static state.
  • On the GC root path, look for the framework-level container (WebAppContext, OSGi Bundle, plugin manager) holding a reference it should have released.
  • ThreadLocal values rooted in pooled threads are a frequent culprit. The thread outlives the redeploy and pins the old loader.
4
Diagnose GC problems and confirm a memory leak across surfaces
▼

GC logs reveal pause-time and throughput trends; heap dumps reveal which objects are retained. Use both together to confirm a leak and identify the culprit.

gc_log__read→check TL;DR + heap trend + reclamation efficiency→heap_dump__read→heap_dump__get_classes (retained)→heap_dump__get_gc_root_path
What to look for
  • A rising live-data trend across consecutive Full GCs is a leak signature, not a sizing issue.
  • Declining reclamation efficiency confirms the heap is filling with objects the collector can't reclaim.
  • Outlier pauses or humongous-allocation events. Drill in with gc_log__get_pause_outliers / gc_log__get_humongous_events before suspecting the heap dump.
  • When citing the result, name both surfaces: "GC log shows live data growing from X to Y MB; heap dump shows class Z retains W% of heap, rooted at S."
5
Find wasted memory from duplicate strings
▼

Applications that parse or intern large volumes of text often accumulate thousands of String instances with identical content, each backed by a separate char array.

heap_dump__get_duplicate_strings (sort: wasted)→heap_dump__get_instances (owner class)→heap_dump__get_inbound_references
What to look for
  • A handful of short, high-cardinality values (status codes, enum-like strings, package names) responsible for most of the waste.
  • Long unique strings duplicated thousands of times are often parsed config, JSON keys, or DB column names.
  • The owning class on inbound references tells you where to intern: a single producer (a parser, a row builder) is easy to fix; many producers means the value should be a flyweight.
  • The fix is usually String.intern(), a dedicated intern cache, or switching to enums / flyweights for high-frequency values.

Tool reference

The full set of MCP tools, grouped by artifact (heap dump, thread dump, GC log) and by function. Everything in this list is also invocable through natural-language prompts; you rarely need to name a tool directly. Paging tools validate their inputs: limit is capped at 10,000 per page (a few tools document a lower maximum, and a documented 0 = all still returns every row), offset is capped at 1,000,000, and out-of-range or fractional values are rejected with an error naming the bound. Each entry below includes an example output, captured from a small com.example demo application and abridged for readability; live output may include more rows and sections as releases evolve.

Heap dump

Multiple dumps at once:heap_dump__read keeps several snapshots in memory, keyed by file path. Per-object tools (get_object_detail, get_instances, get_gc_root_path, and similar) take an optional filePath to choose which loaded dump to act on; it is required once more than one dump is loaded, because object IDs are not unique across dumps. With more than one loaded, object IDs in their output are tagged 0x...@<label> with a short unique per-dump label (the file basename, or a longer path suffix when basenames collide). Pass the tagged id straight back and the label selects the source dump when filePath is omitted (an explicit filePath wins). The tag is applied at the structured id-emission sites, so it is content-safe for --show-strings responses too (a hex-looking class name, field name, or string value is never rewritten). Aggregate tools (get_classes, get_gc_roots, and similar) take an optional filePaths array: omit it (or pass one entry) to query a single dump; two or more entries select diff mode. get_classes renders the class-histogram diff (delta columns, unchanged rows hidden, generated names folded), and the other aggregate tools render added/removed/changed, count, or scalar diffs keyed to their natural identity (or, where that identity is redacted, a non-redacted field such as JMX resource class or agent type). Every aggregate tool now supports diff mode. Use heap_dump__list_snapshots to see what is loaded and heap_dump__close to free one.
Load & Configure
  • heap_dump__read Parse an HPROF file and build the object graph, dominator tree, and retained sizes. Required first step. Takes seconds to minutes depending on dump size. Also reads gzip-compressed heap dumps (.hprof.gz) written by the JDK's own dump tools, with no decompression to disk. The JDK writes these files as many small independent gzip blocks, so deepheap can jump straight to the objects it needs, decompressing about 1 MiB at a time into a bounded in-memory cache (64 MiB by default). The first analysis includes one streaming decompression pass to map the block layout; the map is cached, so reopening the same file skips it. Only multi-block gzip is read directly: a single-block gzip file is not seekable, so the error message says to decompress it first (the decompressed .hprof always works) or re-dump as multi-block gzip via jcmd. Accepts an optional mapping_path to apply a ProGuard/R8 mapping and permanently deobfuscate class and field names on load; if that file cannot be read or parsed, the dump still loads without deobfuscation and the response explains how to retry via heap_dump__reconfigure_snapshot. Only one dump is parsed at a time; a read issued while another load is in progress is rejected with a retry message. Several dumps can be held at once for diffing, but a load is refused when its projected off-heap cost plus the already-loaded dumps would exceed the off-heap budget (default is the smaller of 32 GB and half of physical RAM); the error lists the loaded snapshots so you can close one first.
    Example output

    heap_dump__read(filePath="/data/dumps/demo.hprof")

    Successfully parsed heap dump.

    Layout

    SettingValue
    layoutHotSpot 25 (>= 15 layout)
    compressed_oopstrue
    compressed_class_pointerstrue
    compact_object_headersfalse
    object_header_size12 bytes
    array_base_offset16 bytes
    object_alignment8 bytes
    dominator_exclude_refsnone

    These settings are inferred from the object and pointer sizes in the dump records, not read from a header field (HPROF does not store them). If the inference is wrong because the source JVM ran with different flags, correct them with heap_dump__reconfigure_snapshot; shallow and retained sizes depend on these settings.

    File

    FieldValue
    Path/data/dumps/demo.hprof
    Size213,473,515 bytes
    HPROF timestamp2026-06-07T15:11:43.955Z

    JVM

    FieldValue
    Architecture64-bit
    VersionOpenJDK Runtime Environment (build 25.0.2+10-LTS); OpenJDK 64-Bit Server VM (build 25.0.2+10-LTS, mixed mode)
    JDK buildjava.base@25.0.2
    PlatformmacOS 15.7.4 · x86_64
    VM started2026-06-07T15:11:37.486Z
    JVM uptime6s
    GC algorithmG1GC
    Compact stringstrue
    Integer cache high127

    Heap

    FieldValue
    Classes2,610
    Class loaders (defined ≥1 class)3
    GC roots (records)2,502
    Objects (total)445,853
    Objects bytes (total)204,022,560
    Objects (live)419,643
    Objects bytes (live)55,050,512
    Objects (garbage)26,210
    Objects bytes (garbage)148,972,048
    Loss internal padding (live)~526,130 bytes
    Loss external alignment (live)~393,837 bytes

    Live = reachable via all reference types. Retained/dominator excludes: none.

    Compressed dump compatibility (.hprof.gz)

    Which producers yield a readable .hprof.gz. Only multi-block (multi-member) gzip is supported.

    Produced bySupported
    jcmd <pid> GC.heap_dump -gz=N (JDK 15 or later)Yes
    jmap with the gz option (JDK 16 or later)Yes
    JDK 22+ parallel heap dumps with gzYes
    Azul Zing / Platform Prime jcmd <pid> GC.heap_dump -gz=NYes
    bgzipYes
    gzip (command line)Not directly. Single-block output is not seekable; decompress it first (gunzip) and open the .hprof, or re-dump with jcmd -gz=N
    pigzNot directly. Compresses in parallel but also emits single-block output; decompress it first and open the .hprof
    jhsdb jmap --binaryheap with the gz optionNot directly. The Serviceability Agent writes single-block gzip; decompress it first, or dump uncompressed (uncompressed jhsdb dumps are supported)
    Uncompressed .hprofYes, unchanged
  • heap_dump__reconfigure_snapshot Change JVM layout settings (compressed OOPs, compact headers, per-level field layout, array base offset) and reanalyze without re-reading the file. Any parameter you omit keeps its current effective value, including the auto-detected Azul Zing/C4 corrections (8-byte instance header, 16-byte array base, per-level field packing), so changing one axis never silently reverts the others. Also accepts mapping_path to apply a ProGuard/R8 deobfuscation mapping if one was not supplied at load time. Omitting dominator_exclude_refs (or passing null) keeps the snapshot's current reference filter, so a layout-only or mapping-only reconfigure leaves it unchanged; pass [] to reset it to include all reference types. Use when: retained sizes look off because the snapshot's layout assumptions don't match the dump's source JVM, or when you have a ProGuard mapping you forgot to pass to heap_dump__read.
    Example output

    heap_dump__reconfigure_snapshot(dominator_exclude_refs=["weak", "soft"])

    Snapshot reconfigured and analysis complete.

    Layout

    SettingValue
    layoutHotSpot 25 (>= 15 layout)
    compressed_oopstrue
    compressed_class_pointerstrue
    compact_object_headersfalse
    object_header_size12 bytes
    array_base_offset16 bytes
    object_alignment8 bytes
    dominator_exclude_refsweak, soft

    Heap

    FieldValue
    Classes2,610
    Class loaders (defined ≥1 class)3
    GC roots (records)2,502
    Objects (total)445,853
    Objects bytes (total)204,022,560
    Objects (live)419,643
    Objects bytes (live)55,050,512
    Objects (garbage)26,210
    Objects bytes (garbage)148,972,048
    Loss internal padding (live)~526,130 bytes
    Loss external alignment (live)~393,837 bytes

    Live = reachable via all reference types (mask = 0). Retained/dominator excludes: weak, soft.

    (File and JVM sections unchanged from heap_dump__read; output abridged)

  • heap_dump__list_snapshots List every heap dump currently held in memory, with file size, estimated off-heap RAM cost, load time, JVM layout, and dominator filter. The default snapshot (used when a tool call omits a file path) is marked. The server keeps multiple dumps loaded at once, so this is how you see what is resident. Use when: you have loaded more than one dump and want to check RAM usage or confirm which one tools will act on by default.
    Example output

    heap_dump__list_snapshots()

    Loaded heap dumps (2)

    filePathsizeest. off-heaplayout
    /data/dumps/before.hprof1.2 GB~1.2 GBHotSpot 25 (>= 15 layout)
    */data/dumps/after.hprof1.4 GB~1.4 GBHotSpot 25 (>= 15 layout)

    * marks the default snapshot. Estimated off-heap is a rough multiple of file size; mmap pages are reclaimable and not counted toward the budget.

  • heap_dump__close Free the off-heap memory of one loaded dump. This is the only way to reclaim that RAM short of restarting the server. filePath is resolved against the loaded snapshots, not the filesystem: the absolute registry key shown by heap_dump__list_snapshots, a relative path to the same file, or a unique basename all select the snapshot, so a deleted-but-loaded dump still closes by the key or basename heap_dump__list_snapshots shows. Use when: you are done with an older dump and want its memory back before loading a new large one.
    Example output

    heap_dump__close(filePath="/data/dumps/before.hprof")

    Closed snapshot: /data/dumps/before.hprof

    1 snapshot still loaded; default is now /data/dumps/after.hprof.

Overview & Triage
  • heap_dump__get_summary The baseline summary of an already-loaded dump: the same Layout, heap, object/class/GC-root, JVM, and JDK sections that heap_dump__read returns, rendered without re-parsing. Handy for re-orienting on a dump loaded earlier, or for picking one of several loaded dumps with filePaths. A single-dump summary also reports Valhalla flattening (the JEP 401 value-classes implementation; other Valhalla features are out of scope) whenever an eight-byte-ID dump is not plainly free of the pattern: Layout shows the detected HotSpot generated-ID convention, or an UNSUPPORTED_OR_AMBIGUOUS status with its reason, while Heap shows total, live, and garbage flattened-record subsets after successful corroboration. Flattening is inferred from generated, unaligned HPROF IDs; HPROF contains no direct physical-layout flag. Four-byte-ID dumps predate the pattern and get no report. Use when: you want the headline numbers for a loaded dump again without paying another parse. Passing two or more filePaths renders a scalar diff (the orientation call for comparing dumps).
    Example output

    heap_dump__get_summary(filePaths=["/data/dumps/demo.hprof"])

    Identical to the heap_dump__read summary (Layout / File / JVM / Heap sections); output omitted here for brevity.

    Example output (diff of two dumps)

    heap_dump__get_summary(filePaths=["/data/dumps/before.hprof", "/data/dumps/after.hprof"])

    Summary diff (2 snapshots): before.hprof, after.hprof

    metricbefore.hprofafter.hprofΔ
    Heap size (live)1.1 MB1.2 MB+107.5 KB
    Live objects27,95332,217+4,264
    Classes899900+1
    GC roots811810-1
    JVM vendorAzul Systems, Inc.Azul Systems, Inc.

    A JVM vendor/version mismatch between the dumps would be flagged above the table. Output abridged.

  • heap_dump__get_classes All classes ranked by instance count, shallow size, retained size, or static retained size. Each row renders as className @ 0xHEX so the class object id is available for follow-up calls (handy for inspecting static fields). Each row also shows a Static Retained column (the portion of retained size held by the class object's own static fields and the singletons they keep alive) and retained as a 2-decimal percentage of the live heap. Sort by static_retained to surface static-only leaks that have zero instances but high retention. Supports name filters, regex, and package grouping. On a dump where the recognized Valhalla value-flattening pattern (the JEP 401 value-classes implementation; other Valhalla features are out of scope) is detected, flattening="flattened" keeps only classes with flattened value instances and sortBy="flattened" ranks by that count; either option adds a Flattened column matching the row's scope, so the ordinary aligned population is the instance count minus that column. Both options are rejected on any other dump and in diff mode. When only some of the diffed dumps have flattening detected, the diff header carries a note, because those dumps' instance counts include flattened value records.
    Example output

    heap_dump__get_classes(sortBy="retained", limit=8)

    Classes by total retained size (live):

    #ClassInstancesShallow (B)Retained (B)Static Retained (B)% LiveInternal padding (B)External alignment (B)
    1java.lang.Object[] @ 0x7eb8559c83,350174,97653,062,456096.39%07,904
    2java.util.ArrayList @ 0x7eb8669a01,01324,31252,788,31236095.89%00
    3byte[] @ 0x7eb8453a0163,47246,647,80846,647,808084.74%0370,256
    4java.lang.Class @ 0x7eeab78302,50133,74434,568,248062.79%05,489
    5java.util.HashMap @ 0x7eb8662a042820,54423,307,79227242.34%1,7120
    6java.util.HashMap$Node[] @ 0x7e0fe5bb8412573,18423,300,128042.32%00
    7jdk.internal.loader.ClassLoaders$AppClassLoader @ 0x7e0dcbfa819622,939,4803241.67%30
    8com.example.demo.OrderCache @ 0x7eb8dacd80022,923,56022,923,56041.64%00

    … 2,156 more rows not shown (total shallow: 7.2 MB, total retained: 79.4 MB)

    Example output (diff across three dumps)

    heap_dump__get_classes(filePaths=["/data/dumps/t1.hprof", "/data/dumps/t2.hprof", "/data/dumps/t3.hprof"], sortBy="count")

    Class diff (3 snapshots): t1.hprof, t2.hprof, t3.hprof

    Generated class names (lambdas, CGLIB proxies) are folded; pass normalizeGenerated=false to inspect individual sites.

    #Classt1.hproft2.hproft3.hprofΔcountΔretained
    1java.lang.String8,5559,5538,953+398+18.7 KB
    2java.util.HashMap$Node1,0792,0761,476+397+35.3 KB
    3com.example.demo.SessionEntry100500200+100+1.6 KB
    4+ com.example.demo.PendingRequest02525+25+400 B
    5- com.example.demo.WarmupTask3000-30-480 B

    A column per snapshot, with the first→last Δ. The per-snapshot series is the payoff of multi-dump diff: SessionEntry spiked to 500 then fell back to 200, so its +100 net delta alone would hide the peak. + added, - removed; unchanged classes hidden unless includeUnchanged=true. Sorting is two-axis: sortBy picks the metric (count or retained) and diffRank the statistic over the series. The default diffRank=delta puts the biggest growers first; diffRank=value ranks by the latest dump's footprint (so a large class that barely changed stays on top), and diffRank=churn by the biggest absolute move, either direction. Output abridged.

  • heap_dump__get_class_hierarchy Show the superclass chain and subclasses for any class, with instance count and retained size per level. Every level renders as className @ 0xHEX so the class object id is available for drill-downs (get_instances, get_object_detail, or another get_class_hierarchy call). Pass useRegex=true with anchors for exact-name match.
    Example output

    heap_dump__get_class_hierarchy(className="com.example.demo.Event")

    ## Class hierarchy: com.example.demo.Event (classObjectId=0x7eb8dad58) Resolved from: class name 'com.example.demo.Event' java.lang.Object @ 0x7eb8c0090 [296 inst / 4,928 B retained] └── com.example.demo.Event @ 0x7eb8dad58 [0 inst / 0 B retained] ← self ├── com.example.demo.PaymentEvent @ 0x7eb97c4d0 [200 inst / 8,000 B shallow / 8,000 B retained] └── com.example.demo.OrderEvent @ 0x7eb97b9f8 [200 inst / 6,400 B shallow / 6,400 B retained]
Object Inspection
  • heap_dump__get_instances Find instances of a class (substring or regex match), sorted by retained, shallow, or random. Supports field value filters and subclass inclusion. Omit the class name for heap-wide top-N. The response is prefixed with a Resolved classes line listing every class the name fragment matched, each with its class object id, so you can drill into static-field retention without a second lookup. Pass a list of field names (or "*" for all) to project them as a compact one-row-per-instance table instead of the verbose block, like an OQL SELECT in a single call. On a dump where the recognized Valhalla value-flattening pattern (the JEP 401 value-classes implementation; other Valhalla features are out of scope) is detected, the filter {field:"@flattened", op:"eq", value:true} lists only flattened value instances (false lists only the ordinary aligned ones); it composes with the class name, scope, other filters, sorting, and pagination, and is rejected on any other dump.
    Example output

    heap_dump__get_instances(className="com.example.demo.Order", limit=1)

    Resolved classes: com.example.demo.Order @ 0x7eb8dac60, com.example.demo.OrderCache @ 0x7eb8dacd8, com.example.demo.OrderEvent @ 0x7eb97b9f8 Found 50,200 instances matching 'com.example.demo.Order' (showing 1–1 by retained size): ── Instance 1: 0x7eebdce98 (com.example.demo.Order @ 0x7eb8dac60) ── Shallow: 40 Retained: 360 Fields: | Type | Name | Value | Slot | Shallow | Retained | | --------------------------------------- | --------------- | ------------------------------------------------------------------- | ---: | ------: | -------: | | java.lang.Class<com.example.demo.Order> | <class> | (java.lang.Class<com.example.demo.Order>) @ 0x7eb8dac60 | - | - | 0 | | long | id | 25901 | 8 | - | - | | java.lang.String @ 0x7eb8a118 | status | (String, length=7, backing=byte[], format=ascii-text) @ 0x7eebdcec0 | 4 | 24 | 48 | | com.example.demo.Customer @ 0x7eb8ca0d0 | customer | 0x7eb8ca248 | 4 | 32 | 152 | | byte[256] @ 0x7eb88f40 | payload | 0x7eebdcef0 | 4 | 272 | 272 | | long | createdAtMillis | 1780819196942 | 8 | - | - | … 50,199 more rows not shown (total shallow: 1.9 MB, total retained: 17.2 MB) (use offset=1 to see next page)
  • heap_dump__get_object_detail All fields, types, and values for a single object. Each object-field row renders its value's class in the Type column as className @ 0xHEX (distinct from the instance id in the Value column) so you can drill straight into that class. Class objects also show static fields. Also emits a Context block with the terminal GC root and hop count, immediate dominator (class plus retained size), and inbound reference count by type, so you can pick the next drill-down without an extra round-trip. When the recognized 64-bit HotSpot Valhalla value-flattening pattern (the JEP 401 value-classes implementation) is auto-detected and this object is an instance with an unaligned HPROF id, a Valhalla line is added flagging it as a flattened value.
    Example output

    heap_dump__get_object_detail(objectId="0x7eebdce98")

    Object 0x7eebdce98 Class: com.example.demo.Order @ 0x7eb8dac60 Type: instance Reachable: yes Shallow: 40 bytes Retained: 360 bytes Context: GC root: 8 hops to jni_global → heap_dump__get_gc_root_path for full path Immediate dominator: 0x7eebdce40 java.util.HashMap$Node @ 0x7e0fe5ac8, retained 448 B → heap_dump__get_dominator_tree_node for siblings/children Inbound references: 1 (1 strong) → heap_dump__get_inbound_references for details Fields:
    TypeNameValueSlotShallowRetained
    java.lang.Class<com.example.demo.Order><class>(java.lang.Class<com.example.demo.Order>) @ 0x7eb8dac60--0
    longid259018--
    java.lang.String @ 0x7eb8a118status(String, length=7, backing=byte[], format=ascii-text) @ 0x7eebdcec042448
    com.example.demo.Customer @ 0x7eb8ca0d0customer0x7eb8ca248432152
    byte[256] @ 0x7eb88f40payload0x7eebdcef04272272
    longcreatedAtMillis17808191969428--
  • heap_dump__get_object_contents Render a collection's actual contents: entries of ArrayList, HashMap, IdentityHashMap, ConcurrentHashMap, arrays, StringBuilder, and Scala / Kotlin / Groovy collection types. Note: large Scala immutable.HashMap/HashSet (CHAMP trie, more than 4 entries) and Vector are not yet rendered; use heap_dump__get_object_detail for those. Use when: you've found a suspect cache, queue, or map and want to see what's inside, not just its size. Works on the standard JDK collections plus the major JVM languages' own types.
    Example output

    heap_dump__get_object_contents(objectId="0x7eb8dae10", limit=5)

    - java.util.HashMap @ 0x7eb8dae10 length=50000 showing [0, 5)

    KeyValue
    (String, length=11, backing=byte[], format=ascii-text) @ 0x7eb8dbaa0(com.example.demo.Order) @ 0x7eb8dbad8
    (String, length=11, backing=byte[], format=ascii-text) @ 0x7eb8db8e0(com.example.demo.Order) @ 0x7eb8db918
    (String, length=11, backing=byte[], format=ascii-text) @ 0x7eb8db720(com.example.demo.Order) @ 0x7eb8db758
    (String, length=11, backing=byte[], format=ascii-text) @ 0x7eb8db560(com.example.demo.Order) @ 0x7eb8db598
    (String, length=11, backing=byte[], format=ascii-text) @ 0x7eb8db3a0(com.example.demo.Order) @ 0x7eb8db3d8

    (49995 more - call again with offset=5, limit=5)

  • heap_dump__get_inbound_references Who holds a reference to a given object, sorted by the referrer's retained size.
    Example output

    heap_dump__get_inbound_references(objectId="0x7eebdce98", limit=5)

    Inbound References to 0x7eebdce98 (showing 1–1):

    Object IDClassFieldEdgeShallow (B)Retained (B)
    0x7eebdce40java.util.HashMap$Node @ 0x7e0fe5ac8valuestrong32448
Retention & Reachability
  • heap_dump__get_gc_roots All GC roots grouped by root type, one entry per (root type, object) so an object rooted several ways appears once under each type. Optional type filter restricts to one group (jni_global, jni_local, java_frame, native_stack, sticky_class, thread_block, monitor_used, thread_obj, unknown), and applies in diff mode as well. This lists distinct rooted objects per type; the read summary's "GC roots (records)" counts raw HPROF root records and reads higher.
    Example output

    heap_dump__get_gc_roots(limit=3)

    GC Roots Report (2370 root entries)

    [jni_global] - 128 object(s) (showing 1–3)

    Object IDClassShallow (B)Retained (B)
    0x7eeab77a0java.lang.Class<java.lang.String> @ 0x7eeab783032576
    0x7eeab91d0sun.nio.cs.UTF_8 @ 0x7e0fe57382448
    0x7eeab9010java.lang.Thread @ 0x7eeab8038104232

    … 125 more rows not shown (total shallow: 112.5 KB, total retained: 22.0 MB)

    (use offset=3 type="jni_global" to see next page)

    [sticky_class] - 2226 object(s) (showing 1–3)

    Object IDClassShallow (B)Retained (B)
    0x7ff805b98java.lang.Class<jdk.internal.vm.SharedThreadContainer$$Lambda+0x000000076e1d2c00> @ 0x7eeab783000
    0x7ffafc880java.lang.Class<java.util.stream.StreamSpliterators$WrappingSpliterator$$Lambda+0x000000076e1d2948> @ 0x7eeab783000
    0x7ffaed0f0java.lang.Class<jdk.internal.vm.ThreadDumper$$Lambda+0x000000076e1d2708> @ 0x7eeab783000

    … 2,223 more rows not shown (total shallow: 32.2 KB, total retained: 1.1 MB)

    (use offset=3 type="sticky_class" to see next page)

    (1 more root-type group omitted; output abridged)

    Example output (diff of two dumps)

    heap_dump__get_gc_roots(filePaths=["/data/dumps/before.hprof", "/data/dumps/after.hprof"])

    Root type diff (2 snapshots): before.hprof, after.hprof

    #Root typebefore.hprofafter.hprofΔcountΔretained
    1jni_global1425,406+5,264+84.3 MB
    2java_frame311408+97+2.4 MB
    3thread_obj2496+72+6.1 MB
    4+ monitor_used012+12+48 KB

    Roots grouped by type, each keyed to its per-snapshot count and summed retained. Here a JNI global-reference leak dominates (jni_global +5,264). + marks a type that appeared; unchanged types hidden unless includeUnchanged=true. The count-style diffs accept diffRank too: default delta (biggest growth), value (most numerous now), or churn (biggest movers either direction). Illustrative.

  • heap_dump__get_gc_root_path The exact reference chain from a GC root to a target object. Answers "why is this still alive?". By default excludes weak, soft, phantom, and finalizer edges (matching MAT's path2gc); override with exclude_refs. Synthetic edges that no declared Java field carries (a thread holding an object via its stack, a GC-root category node holding the object it roots, an object's reference to its class, classloader, or superclass) render with a bracketed label like [stack-local], [thread-root], [gc-root], or [class]. Array-element references render as [index] (the array slot), since an array slot has no field name.
    Example output

    heap_dump__get_gc_root_path(objectId="0x7eebdce98")

    GC Root Path to 0x7eebdce98 (9 steps): GC Root: jni_global [shallow=0 B / retained=0 B] └── [gc-root] jdk.internal.loader.ClassLoaders$AppClassLoader @ 0x7eeab9dc0 (class @ 0x7e0dcbfa8) [shallow=96 B / retained=22,939,448 B] └── .classes (strong) java.util.ArrayList @ 0x7eb97b9a8 (class @ 0x7eb8669a0) [shallow=24 B / retained=22,924,168 B] └── .elementData (strong) java.lang.Object[15] @ 0x7ffe031c8 (class @ 0x7eb8559c8) [shallow=80 B / retained=22,924,144 B] └── [3] java.lang.Class @ 0x7eb8dacd8 (class @ 0x7eeab7830) [shallow=8 B / retained=22,923,560 B] └── .CACHE (strong) java.util.HashMap @ 0x7eb8dae10 (class @ 0x7eb8662a0) [shallow=48 B / retained=22,923,552 B] └── .table (strong) java.util.HashMap$Node[131072] @ 0x7eb8e4038 (class @ 0x7e0fe5bb8) [shallow=524,304 B / retained=22,923,504 B] └── [88301] java.util.HashMap$Node @ 0x7eebdce40 (class @ 0x7e0fe5ac8) [shallow=32 B / retained=448 B] └── .value (strong) com.example.demo.Order @ 0x7eebdce98 (class @ 0x7eb8dac60) [shallow=40 B / retained=360 B] ← target
  • heap_dump__get_dominator_tree_node Navigate the dominator tree. For any node, list the objects it exclusively retains, sorted by retained size. Each retained size is also shown as a 2-decimal share of the live heap. Use when: the top-retained class list doesn't tell the full story and you want to drill into the actual object holding everything together. Start at a suspect instance and walk down to see what it's keeping alive.
    Example output

    heap_dump__get_dominator_tree_node(objectId="0x7eb8dae10", limit=5)

    Dominator Tree Node: 0x7eb8dae10 Class: java.util.HashMap @ 0x7eb8662a0 Parent: 0x7eb8dacd8 (java.lang.Class @ 0x7eeab7830) Shallow: 48 bytes Retained: 22,923,552 bytes (41.64% of live) Children: 1 total (showing 1–1)
    #Object IDClassShallow (B)Retained (B)% LiveChildren
    10x7eb8e4038java.util.HashMap$Node[131072] @ 0x7e0fe5bb8524,30422,923,50441.64%40,773
Native Memory & System Resources
  • heap_dump__get_off_heap_memory All DirectByteBuffer, NativeMemorySegment, and MappedMemorySegment allocations with sizes and addresses.
    Example output

    heap_dump__get_off_heap_memory()

    Off-Heap Memory Report Total off-heap: 342,617,109 bytes Unique bytes (deduplicated): 196,883,576 bytes Allocations found: 19 (showing 1–19) Derived/shared: 3 allocation(s) are views into another buffer's memory By category: DirectByteBuffer 342,617,109 bytes
    #Object IDClassCategoryNative AddrCapacity (B)Retained (B)DerivedFile
    10x7e0afa640java.nio.DirectByteBufferR @ 0x7e08f1ec8DirectByteBuffer0x2d278e000144,413,76964*
    20x7e0afa680java.nio.DirectByteBuffer @ 0x7eeaba670DirectByteBuffer0x2d278e000144,413,76964
    30x7eb987220java.nio.DirectByteBuffer @ 0x7eeaba670DirectByteBuffer0x7fa6ee40000016,777,21664
    40x7eb987370java.nio.DirectByteBuffer$Deallocator @ 0x7ee702e70DirectByteBuffer0x7fa6ee40000016,777,21632
    50x7eea51b50java.nio.DirectByteBuffer @ 0x7eeaba670DirectByteBuffer0x2d05e50008,388,60864
    60x7eb987288java.nio.DirectByteBuffer @ 0x7eeaba670DirectByteBuffer0x7fa6ee0000004,194,30464
    70x7eb987350java.nio.DirectByteBuffer$Deallocator @ 0x7ee702e70DirectByteBuffer0x7fa6ee0000004,194,30432
    80x7eb9872f0java.nio.DirectByteBuffer @ 0x7eeaba670DirectByteBuffer0x7fa6ed1b00001,048,57664
    … 11 more rows (abridged)
  • heap_dump__get_network_connections TCP/UDP sockets (each with its instance object id and class), DNS configuration, JDWP port, and detected HTTP client libraries shown as "class @ 0xaddr" for navigation.
    Example output

    heap_dump__get_network_connections()

    Network Connections Report

    Listening Ports (1):

    Object IDLocal AddressClass
    0x7eea53ff80.0.0.0:51583java.net.ServerSocket @ 0x7eea41958

    Active TCP Connections (1):

    Object IDLocalRemoteClass
    0x7eeaa1c98*:51583127.0.0.1:51584java.net.Socket @ 0x7eea84ff0

    UDP Sockets (1):

    Object IDLocal AddressClass
    0x7eea6d3100:0:0:0:0:0:0:0:57059sun.nio.ch.DatagramChannelImpl @ 0x7eea399b8

    JDWP: not detected

    DNS Nameservers: not detected

    HTTP Client classes: none detected

  • heap_dump__get_threads All threads with stack traces and retained sizes (also shown as a 2-decimal share of the live heap), sortable by retained or name. Each row reports the thread state (RUNNABLE, WAITING, TIMED_WAITING, BLOCKED), daemon flag, and priority when the dump records them, including JDK 19+ dumps where those fields moved into the thread's field holder. Compact summary mode and per-thread frame_limit keep large dumps inline; auto-switches to summary if the full response would exceed the inline cap.
    Example output

    heap_dump__get_threads(limit=3, frame_limit=5)

    Threads (16 total, showing 1–3, sorted by retained size) Thread "batch-worker-1" [java.lang.Thread @ 0x7eeab8038] state=WAITING [alive, waiting, waiting indefinitely, parked] daemon=true priority=5 retained=6,291,824 bytes (11.43% of live), shallow=104 bytes (0x7eea519c8) at jdk.internal.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at com.example.demo.DemoApp$$Lambda+0x000000076e109a30.run(<unknown>) at java.lang.Thread.runWith(Thread.java:1487) ... (1 more frames; pass frame_limit=0 for full stack) Thread "batch-worker-3" [java.lang.Thread @ 0x7eeab8038] state=WAITING [alive, waiting, waiting indefinitely, parked] daemon=true priority=5 retained=6,291,824 bytes (11.43% of live), shallow=104 bytes (0x7eea6ac90) at jdk.internal.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at com.example.demo.DemoApp$$Lambda+0x000000076e109a30.run(<unknown>) at java.lang.Thread.runWith(Thread.java:1487) ... (1 more frames; pass frame_limit=0 for full stack) Thread "batch-worker-2" [java.lang.Thread @ 0x7eeab8038] state=WAITING [alive, waiting, waiting indefinitely, parked] daemon=true priority=5 retained=6,291,824 bytes (11.43% of live), shallow=104 bytes (0x7eea38718) at jdk.internal.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at com.example.demo.DemoApp$$Lambda+0x000000076e109a30.run(<unknown>) at java.lang.Thread.runWith(Thread.java:1487) ... (1 more frames; pass frame_limit=0 for full stack) … 13 more rows not shown (total shallow: 1.3 KB, total retained: 12.1 KB) (use offset=3 to see next page)
ClassLoaders & Duplicates
  • heap_dump__get_class_loaders All ClassLoader subtypes with their JAR/URL paths. Filters JDK builtin loaders by default (pass excludeJdk=false to include) and supports className and loaderName substring filters. The excludeJdk, className, and loaderName filters apply in diff mode as well. The footer reconciles two counts: total ClassLoader instances on the heap, and how many defined at least one class (the figure shown in the read summary).
    Example output

    heap_dump__get_class_loaders()

    Class Loader Report

    Class Loaders (2):

    Object IDClassClassesLoader NameParent IDURLs
    0x7eea38930java.net.URLClassLoader @ 0x7eea394181plugin-loader-20x7eeab9dc0/opt/order-service/plugins/demo-plugin.jar
    0x7eea38a58java.net.URLClassLoader @ 0x7eea394181plugin-loader-10x7eeab9dc0/opt/order-service/plugins/demo-plugin.jar

    (filtered 3 JDK builtin loader instances; pass excludeJdk=false to include)

    Loader counts: 5 ClassLoader instances; 3 defined ≥1 class.

  • heap_dump__get_duplicate_classes Classes loaded by more than one ClassLoader. A common cause of ClassCastException in OSGi and plugin frameworks.
    Example output

    heap_dump__get_duplicate_classes()

    Duplicate class definitions (1 classes loaded by multiple ClassLoaders):

    ClassDefsInstancesShallow (B)Retained (B)
    com.example.plugin.PluginTask2232352
  • heap_dump__get_duplicate_strings String instances with identical content backed by separate char arrays, wasting heap. Sorted by wasted bytes. In diff mode (two or more dumps) it compares live-only wasted-byte totals, and includeUnreachable is not supported.
    Example output

    heap_dump__get_duplicate_strings(sortBy="wasted", limit=5)

    Duplicate String Groups: 1,255 groups, 356.0 KB total wasted (showing 1–5)

    Sorted by: wasted bytes (best dedup ROI first)

    #InstancesArraysLength (chars)EncodingWasted (B)Total (B)Sample String Object IDs
    116,66816,6687LATIN1116,669116,6760x7e0000190, 0x7e0000890, 0x7e0000dd0 (+16665 more)
    216,66716,6677LATIN1116,662116,6690x7e00006d0, 0x7e0000c10, 0x7e0001310 (+16664 more)
    316,66816,6686LATIN1100,002100,0080x7e0000350, 0x7e0000510, 0x7e0000a50 (+16665 more)
    4181843LATIN17317740x7eb9d8cc0, 0x7eb9d8d80, 0x7eb9d9230 (+15 more)
    5393913LATIN14945070x7eb9d3110, 0x7eb9d5fd0, 0x7eb9f4298 (+36 more)

    … 1,250 more groups not shown (total wasted: 29.3 KB)

    (use offset=5 to see next page)

    Note: String content withheld (security).

System Metadata
  • heap_dump__get_system_properties JVM system properties as key=value pairs (sensitive values redacted by default).
    Example output

    heap_dump__get_system_properties()

    JVM System Properties (56 entries):

    KeyValue
    file.encodingUTF-8
    java.version25.0.2
    java.vm.nameOpenJDK 64-Bit Server VM
    java.vm.vendorEclipse Adoptium
    os.nameMac OS X
    os.version15.7.4
    sun.java.command(String, REDACTED) @ 0x7eb8a08f8
    user.dir(String, REDACTED) @ 0x7eb8a1370
    … 48 more rows (abridged)
    Example output (diff of two dumps)

    heap_dump__get_system_properties(filePaths=["/data/dumps/before.hprof", "/data/dumps/after.hprof"])

    System properties diff (2 snapshots): before.hprof, after.hprof

    keybefore.hprofafter.hprof
    ~java.runtime.version21.0.4+7-LTS21.0.7+9-LTS
    ~java.version21.0.421.0.7
    +app.rollout.canary(absent)(String, REDACTED)
    -jdk.tls.client.protocols(String, REDACTED)(absent)

    Grouped changed / added (+) / removed (-); (absent) = absent in that dump. Values are redacted per-side exactly as in single-dump mode, so a change surfaces only when the visible value changes (a JVM patch here). Unchanged keys hidden unless includeUnchanged=true. Illustrative.

  • heap_dump__get_system_environment OS environment variables visible to the JVM process.
    Example output

    heap_dump__get_system_environment()

    OS Environment Variables (37 entries):

    KeyValue
    HOME(String, REDACTED) @ 0x7ffe4ca30
    JAVA_HOME(String, REDACTED) @ 0x7ffe4b9d8
    LANG(String, REDACTED) @ 0x7ffe4bf70
    PATH(String, REDACTED) @ 0x7ffe4c570
    SHLVL2
    USER(String, REDACTED) @ 0x7ffe4b530
    … 31 more rows (abridged)
  • heap_dump__get_jfr_recordings Active JFR (Java Flight Recorder) recordings, their state, and event settings. The Object line is the jdk.jfr.internal.PlatformRecording instance, and the Settings line shows the settings map as "class @ 0xaddr" so you can navigate to it. With the injection gate on, the recording name is redacted to its heap address (so you can still navigate to it), settings collapse to an entry count, and the state is shown only when it matches a known RecordingState value.
    Example output

    heap_dump__get_jfr_recordings()

    JFR Recordings: 1 found Recording #1: "(String, REDACTED) @ 0x6c1a3f8d0" Object: jdk.jfr.internal.PlatformRecording @ 0x7eb88ab18 Recording ID: 1 State: RUNNING To disk: true Dump on exit: false Max size: unlimited Duration: unlimited Max age: unlimited Settings: java.util.LinkedHashMap @ 0x6c1a41200 (3 entries, content redacted by the string-hiding gate)
  • heap_dump__get_java_agents Attached Java agents loaded via -javaagent, -agentpath, or -agentlib flags. The Source column shows the heap object each entry came from as "class @ 0xaddr" (the JVM-arg String, or the InstrumentationImpl instance), so you can navigate to it. With the injection gate on, the agent name, path, and options are redacted; the type and Source columns are always shown.
    Example output

    heap_dump__get_java_agents()

    Java Agents: 1 found

    TypeAgent NamePathOptionsSource
    instrumentation<unknown><detected>sun.instrument.InstrumentationImpl @ 0x7c0044e80
  • heap_dump__get_jmx_mbeans JMX MBean attributes (primitives, Strings, AtomicLong/Integer), filterable by domain or object name (both filters apply in diff mode as well). The resource class is shown as "class @ 0xaddr", where the address is the resource instance whose fields are the listed attributes. Compact summary mode groups MBeans by domain without attribute values; auto-switches to summary if the full response would exceed the inline cap. With the injection gate on, MBean object names, map-entry keys, and non-numeric attribute values are redacted; plain attribute names and resource class names are class metadata and stay visible.
    Example output

    heap_dump__get_jmx_mbeans(domain="com.example")

    JMX MBeans (1 found, domain filter: com.example): ── com.example:type=CacheStats (com.example.demo.CacheStats @ 0x7f3a210b8) ── cachedOrders 50000 hitRate 0.97 evictionCount 1342

    (output abridged)

  • heap_dump__get_modules Java modules (JPMS) loaded by the JVM, with the Module instance object id, module name, version, main class, the ClassLoader that defines each module, and a count of the module's ServiceLoader bindings. The Provides column counts the provides <service> with <impl> directives from each module-info, and a detail section below the table expands them to service -> provider classes. This surfaces the service providers registered on the module path (JDBC drivers, filesystem providers, application SPIs). It does not cover classpath (META-INF/services) providers, which are not stored in a ModuleDescriptor. Returns empty on JDK 8 and earlier, where the module system was not yet introduced.
    Example output

    heap_dump__get_modules(limit=8)

    Java Modules (70) (showing 1–8):

    Module NameObject IDVersionMain ClassLoader Object IDProvides
    (unnamed)0x7eeac1100--(bootstrap)-
    jdk.compiler0x7eeac134025.0.2-0x7eeab9dc02
    java.logging0x7eeac158025.0.2-(bootstrap)-
    (unnamed)0x7eeac17c0--(bootstrap)-
    jdk.proxy10x7eeac1a00--(bootstrap)-
    jdk.jfr0x7eeac1c4025.0.2jdk.jfr.internal.tool.Main(bootstrap)-
    jdk.management.jfr0x7eeac1e8025.0.2-(bootstrap)-
    jdk.management0x7eeac20c025.0.2-(bootstrap)1

    … 62 more rows not shown

    (use offset=8 to see next page)

    Service providers (module -> service -> providers):

    - jdk.compiler:
      - com.sun.source.util.Plugin -> (none)
      - java.util.spi.ToolProvider -> com.sun.tools.javac.main.JavacToolProvider
    - jdk.management:
      - sun.management.spi.PlatformMBeanProvider -> com.sun.management.internal.PlatformMBeanProviderImpl
  • heap_dump__get_service_loaders Live java.util.ServiceLoader instances in the heap, with the service type each loads, its defining ClassLoader, and the providers it discovered. A detail section expands each loader as service -> provider class [instance], marking whether each provider was actually instantiated or only discovered. This is the runtime, resolved view of the JDK ServiceLoader mechanism. Unlike get_modules (which reports the static module-info provides directives), it also covers classpath (META-INF/services) providers, but only for services the application actually loaded.
    Example output

    heap_dump__get_service_loaders(limit=8)

    Java ServiceLoaders (12) (showing 1–8):

    ServiceServiceLoader Object IDLoader Object IDProviders
    java.nio.file.spi.FileSystemProvider0x7ef012340(bootstrap)2
    java.time.chrono.Chronology0x7ef014880(bootstrap)4
    org.example.spi.ReportRenderer0x7ef0161c00x7ee9ad2001

    … 9 more rows not shown

    (use offset=8 to see next page)

    Providers (service -> provider [instance]):

    - org.example.spi.ReportRenderer:
      - org.example.impl.HtmlReportRenderer @ 0x7ee9c4410 (instantiated)
      - org.example.impl.PdfReportRenderer (not instantiated)

Thread dump

Load & Inspect
  • thread_dump__read Parse a jstack/jcmd text dump or a JEP 425 JSON dump (jcmd Thread.dump_to_file -format=json). The JSON format scales to millions of virtual threads (Loom). Required first step. Returns total thread count, breakdown by state and container, runtime version, dump timestamp, format, and top stack-trace clusters. Input must be a regular file (fifos, sockets, and char devices are rejected) and must be plain text; files with non-text bytes in the first 4 KB are rejected. Default size cap is 256 MB, configurable via -Dio.deepheap.threadDump.maxFileBytes=<bytes> (0 disables).
    Example output

    thread_dump__read(filePath="/data/dumps/demo.threads.txt")

    Thread dump summary

    PropertyValue
    Threads20
    Virtual0
    FormatTEXT
    RuntimeFull thread dump OpenJDK 64-Bit Server VM (25.0.2+10-LTS mixed mode):
    Dump time2026-06-07 11:11:43

    Threads by state:

    StateCount
    RUNNABLE11
    WAITING (parking)4
    TIMED_WAITING (on object monitor)2
    BLOCKED (on object monitor)1
    TIMED_WAITING (sleeping)1
    WAITING (on object monitor)1
    Top stack-trace clusters (threads sharing same top frames): Cluster 1 - 8 threads (e.g. Signal Dispatcher, Service Thread, Monitor *** Thread): Cluster 2 - 3 threads (e.g. batch-worker-1, batch-worker-2, batch-worker-3): at jdk.internal.misc.Unsafe.park(java.base@25.0.2/Native Method) at java.util.concurrent.locks.LockSupport.park(java.base@25.0.2/LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at 0x000000076e109a30.run(Unknown Source) at java.lang.Thread.runWith(java.base@25.0.2/Thread.java:1487)
  • thread_dump__list_threads Paginated thread list with name, tid, state, virtual flag, container, and stack frames. Filter by name substring or state (RUNNABLE / WAITING / BLOCKED).
    Example output

    thread_dump__list_threads(nameFilter="batch-worker", limit=3)

    Thread dump threads (3 total, showing 1–3) Thread "batch-worker-1" tid=140354837821952 [daemon] state=WAITING (parking) prio=5 at jdk.internal.misc.Unsafe.park(java.base@25.0.2/Native Method) at java.util.concurrent.locks.LockSupport.park(java.base@25.0.2/LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at 0x000000076e109a30.run(Unknown Source) at java.lang.Thread.runWith(java.base@25.0.2/Thread.java:1487) at java.lang.Thread.run(java.base@25.0.2/Thread.java:1474) Thread "batch-worker-2" tid=140354837824000 [daemon] state=WAITING (parking) prio=5 at jdk.internal.misc.Unsafe.park(java.base@25.0.2/Native Method) at java.util.concurrent.locks.LockSupport.park(java.base@25.0.2/LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at 0x000000076e109a30.run(Unknown Source) at java.lang.Thread.runWith(java.base@25.0.2/Thread.java:1487) at java.lang.Thread.run(java.base@25.0.2/Thread.java:1474) Thread "batch-worker-3" tid=140354837809664 [daemon] state=WAITING (parking) prio=5 at jdk.internal.misc.Unsafe.park(java.base@25.0.2/Native Method) at java.util.concurrent.locks.LockSupport.park(java.base@25.0.2/LockSupport.java:223) at com.example.demo.DemoApp.lambda$startWorkerThreads$0(DemoApp.java:281) at 0x000000076e109a30.run(Unknown Source) at java.lang.Thread.runWith(java.base@25.0.2/Thread.java:1487) at java.lang.Thread.run(java.base@25.0.2/Thread.java:1474)

GC log

Load & Diagnose
  • gc_log__read Parse a Java GC log file and return a full diagnostic report. Accepts plain text, gzipped (.gz), and rotating log sets (auto-detects siblings like gc.log.0..N and .current in the same directory; disable with findRotated=false). Auto-detects unified (JDK 9+) and pre-unified (JDK 8) formats; supports G1, Parallel, Serial, CMS, ZGC, and Shenandoah, plus Azul Zing C4 (basic metrics). The response includes a TL;DR, collector overview, throughput, pause stats by category, cause breakdown, heap trend, allocation/promotion rates, reclamation efficiency, concurrent phase stats, G1 region stats, heap sizing recommendation, CPU time, and safepoint stats, all in one call. When --root is set (either transport), both findRotated=true and directory inputs are refused (rotation directory walking cannot be safely contained); pass findRotated=false and a single file path per call. Plain-text inputs must begin with a recognizable GC log header (unified [timestamp]/[uptime] or pre-unified date stamp/uptime) within the first 4 KB; non-text or unrecognized files are rejected. Default size cap is 4 GB per file, configurable via -Dio.deepheap.gcLog.maxFileBytes=<bytes> (0 disables).
    Example output

    gc_log__read(filePath="/data/dumps/demo.gc.log")

    TL;DR

    - Heap trend: GROWING - 104.0 MB → 128.0 MB across 4s (consider a heap dump from this JVM). - Humongous allocations: 61 events, peak 112 regions (2.0 MB each, threshold ≥ 1.0 MB) - peak live humongous footprint ≈ 224.0 MB in old gen (split across an unknown number of objects, each ≥ 1.0 MB; GC logs do not report per-object sizes) - consider larger -XX:G1HeapRegionSize or reduce large object allocation size.

    File

    PropertyValue
    Path/data/dumps/demo.gc.log
    Size749.8 KB
    Last modified2026-06-07T15:14:00.057Z
    Files parsedSingle file
    Log starts2026-06-07T15:13:55.076Z (uptime 0.353s)
    Log ends2026-06-07T15:13:59.996Z (uptime 5.273s)
    Log span4s

    GC Summary

    MetricValue
    CollectorG1
    FormatUnified (JDK 9+)
    Total events480
    STW events180
    Throughput96.25%
    Total STW time184ms
    G1 heap region size2.0 MB
    Humongous threshold≥ 1.0 MB

    Pause Summary by Category

    CategoryCountMinp50p95p99MaxTotal
    Young600.9 ms1.7 ms3.7 ms5.1 ms5.1 ms117ms
    Full GC13.4 ms3.4 ms3.4 ms3.4 ms3.4 ms3ms
    Remark / InitialMark1190.1 ms0.6 ms1.1 ms1.3 ms1.4 ms64ms
    Concurrent3000.0 ms0.5 ms1.7 ms2.9 ms169.6 ms499ms

    Note: Concurrent phase durations include wall-clock time, not application pause time. Throughput is computed from STW categories only.

    Heap Trend (after-GC occupancy)

    MetricValue
    TrendGROWING
    Start104.0 MB
    End128.0 MB
    Data points180
    Mean124.7 MB
    p95179.0 MB
    Peak224.0 MB

    Growing live data across the log duration - consider loading a heap dump captured from the same JVM to identify the leaking class.

    (report continues: cause breakdown, allocation/promotion rates, reclamation efficiency, G1 region stats, sizing recommendation, CPU and safepoint stats; output abridged)

Drill-Down
  • gc_log__get_pause_outliers Top-N STW events exceeding a duration threshold (default: max(200 ms, p99 × 2)), with timestamp, cause, heap before/after, and sys/user CPU time. Use when the report shows extreme individual pauses that need per-event detail.
    Example output

    gc_log__get_pause_outliers(thresholdMs=2, limit=5)

    Top pause outliers (threshold 2 ms, showing 5 of 24)

    #DurationCategoryCauseTimestampHeap BeforeHeap AfterUser CPUKernel CPU
    15.1 msYoungG1 Humongous Allocation1.628 s231.0 MB102.0 MB0.010 s0.000 s
    23.9 msYoungG1 Humongous Allocation1.296 s231.0 MB98.0 MB0.000 s0.000 s
    33.7 msYoungG1 Humongous Allocation0.353 s232.0 MB104.0 MB0.000 s0.010 s
    43.6 msYoungG1 Humongous Allocation0.449 s231.0 MB98.0 MB0.000 s0.000 s
    53.4 msFull GCHeap Dump Initiated GC3.835 s181.0 MB112.0 MB0.010 s0.010 s

    … 19 more outliers not shown

  • gc_log__get_safepoint_outliers Top-N non-GC safepoint events (RevokeBias, ThreadDump, Deoptimize, …) exceeding a stopped-time threshold (default: max(50 ms, p99 × 2)), with trigger name, TTSP, and timestamp. GC pauses are excluded. Use when the TL;DR flags a costly non-GC safepoint trigger.
    Example output

    gc_log__get_safepoint_outliers(thresholdMs=0.1, limit=5)

    Top safepoint outliers (threshold 0 ms, showing 5 of 8)

    #StoppedTriggerTTSPTimestamp
    10.4 msRevokeBias0.3 ms3.108 s
    20.2 msRevokeBias0.2 ms0.166 s
    30.2 msRevokeBias0.1 ms3.108 s
    40.2 msEnableBiasedLocking0.1 ms0.120 s
    50.2 msRevokeBias0.0 ms3.109 s

    … 3 more outliers not shown

  • gc_log__get_humongous_events Top-N G1 pause events ranked by the per-pause snapshot count of humongous regions live in old gen, with cause, timestamp, duration, and heap before/after. The count is a snapshot, not a per-pause allocation delta (one large object can appear in many rows until reclaimed). G1 only. Use when the TL;DR flags a large humongous footprint in old gen.
    Example output

    gc_log__get_humongous_events(limit=5)

    Top humongous events (min 1 region, showing 5 of 61)

    #Hum. regions liveBefore → AfterCauseTimestampDurationHeap Before → After
    1112112 → 112G1 Humongous Allocation4.080 s1.3 ms225.0 MB → 224.0 MB
    284114 → 84G1 Humongous Allocation3.828 s2.0 ms229.0 MB → 168.0 MB
    384114 → 84G1 Humongous Allocation2.481 s1.2 ms229.0 MB → 168.0 MB
    483113 → 83G1 Humongous Allocation3.478 s2.0 ms227.0 MB → 166.0 MB
    583113 → 83G1 Humongous Allocation2.120 s1.7 ms227.0 MB → 166.0 MB

    … 56 more events not shown

    `Hum. regions live` is the snapshot count of regions occupied by humongous objects at this GC - not the size or count of objects allocated this pause. A single large object survives across many GCs and shows up in every row until reclaimed. To learn the size of any individual humongous object, load a heap dump and inspect arrays by retained size.

  • gc_log__get_allocation_spikes Top-N allocation-rate intervals (inter-Young-GC windows) exceeding a rate threshold (default: max(500 MB/s, p99 × 1.5)), with rate, bytes allocated, window timestamps, and duration. Use when the TL;DR flags a spikey or high allocation rate.
    Example output

    gc_log__get_allocation_spikes(minRateMbs=100, limit=5)

    No allocation intervals exceeded 100 MB/s (threshold = max(500, p99×1.5) = 100 MB/s).

FAQ & troubleshooting

Common setup, debugging, and update questions.

?
My MCP client doesn't see the server
▼
  • Double-check the absolute path to the mcp binary in your client config. Relative paths and ~ aren't expanded by most clients.
  • Restart the client after editing the config; most MCP clients only read it on launch.
  • Run the binary manually first: /path/to/deepheap/bin/mcp --help should print usage. If it doesn't, the install is incomplete.
  • On macOS, you may need to remove the quarantine attribute: xattr -d com.apple.quarantine /path/to/mcp.
  • Check your client's MCP log. Most clients surface stderr from the server there, which usually identifies the issue immediately.
?
Heap dump load is slow / running out of memory
▼
  • deepheap is out-of-core: it memory-maps the dump rather than loading the full file into RAM, and spills its largest derived indexes to disk. Footprint scales with object count, not file size, so dumps far larger than RAM are routine. Dumps with very high object counts (billions) need more headroom for the dominator build; if a load struggles, check for swap pressure or anti-virus scanners reading the file.
  • The initial heap_dump__read call builds the dominator tree, which is the expensive step. Multi-gigabyte dumps can take several minutes.
  • Subsequent calls are fast; the parsed snapshot is cached and reused for the lifetime of the deepheap process.
  • SSDs make a large difference. The graph build is bound on random read latency more than throughput.
?
How do I capture a heap dump?
▼

Use jcmd on the running JVM. Find the PID with jps, then:

jcmd <pid> GC.heap_dump /path/to/app.hprof

By default this triggers a full GC first, so only live objects end up in the dump. That's usually what you want for leak analysis.

To capture everything on the heap, including unreachable objects waiting to be collected, skip the GC:

jcmd <pid> GC.heap_dump -all /path/to/app.hprof

The post-GC dump is also significantly smaller on disk, since unreachable objects are gone before the snapshot is written. If you do capture a -all dump, deepheap can still narrow analysis to reachable objects: most tools accept a scope parameter (live / garbage / all), so you can keep the full dump for forensics and filter at query time.

Alternative: -XX:+HeapDumpOnOutOfMemoryError as a JVM flag writes a dump automatically the next time the JVM throws OutOfMemoryError.

?
How do I capture a thread dump?
▼

Use jcmd on the running JVM. Find the PID with jps, then:

For a plain-text thread dump:

jcmd <pid> Thread.print > /path/to/threads.txt

For the JSON format (JEP 425, JDK 21+), which is the only one that scales to millions of virtual threads:

jcmd <pid> Thread.dump_to_file -format=json /path/to/threads.json

If you suspect a deadlock or a stuck thread, take three dumps about 5 seconds apart. Comparing them shows which threads are actually moving and which are pinned in one spot.

?
How do I enable GC logging?
▼

On JDK 9+, use the unified logging framework. Add this to the JVM command line:

-Xlog:gc*,gc+ref=debug,gc+phases=debug,gc+age=trace,safepoint:file=gc-%t.log:time,uptime,level,tags:filecount=10,filesize=50M

What each piece does:

  • gc*: all GC events at info level.
  • gc+ref=debug: reference processing (weak/soft/phantom) timings, often a hidden source of pause time.
  • gc+phases=debug: per-phase breakdown of each pause.
  • gc+age=trace: object aging in young generations, useful for tenuring threshold tuning.
  • safepoint: safepoint sync and stop times, which catch non-GC pauses.
  • file=gc-%t.log: an absolute or relative path; %t expands to a timestamp so restarts don't overwrite. Use file=/var/log/myapp/gc-%t.log for a fixed location.
  • filecount=10,filesize=50M: rotate after 50 MB, keep 10 files.

On JDK 8 the equivalent is -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M. deepheap reads both formats.

?
What heap dump and log formats are supported?
▼
  • Heap dumps: HotSpot HPROF binary only, what you get from jmap -dump:format=b, jcmd <pid> GC.heap_dump, or -XX:+HeapDumpOnOutOfMemoryError on a HotSpot-based JVM (OpenJDK, Oracle JDK, Adoptium Temurin, Amazon Corretto, Azul Zulu, etc.). JDK 8 dumps through JDK 25 dumps all work. Azul Zing / Platform Prime dumps also load: Zing uses a different object model, which deepheap detects and sizes correctly, and its C4 collector is identified in the summary. IBM J9 / OpenJ9 PHD dumps and GraalVM native-image heap dumps are not supported (GraalVM running on HotSpot emits standard HPROF, which loads).
  • JVM languages: the HPROF format is language-agnostic, so dumps from Scala, Kotlin, Groovy, Clojure, JRuby, Jython, etc. all load. deepheap has dedicated collection renderers for Scala, Kotlin, and Groovy types (cons lists, Map1–4, Set1–4, Tuple, Option, Either, Pair, Triple, GString, …) so their contents render as entries rather than internal Java fields. Detected languages and versions appear in the summary that heap_dump__read returns after loading.
  • Thread dumps: jstack / jcmd Thread.print text format, and the JEP 425 JSON format (jcmd Thread.dump_to_file -format=json). The JSON format is the only one that scales to millions of virtual threads.
  • GC logs: plain text and gzipped (.gz), unified (JDK 9+) and pre-unified (JDK 8). Collectors: G1, Parallel, Serial, CMS (removed in JDK 14, logs still parse), ZGC, Shenandoah, Azul Zing C4 (basic metrics).
?
Where does my heap data go? Is anything sent off-machine?
▼
  • The deepheap process runs entirely on your machine and does not phone home.
  • But your AI client does. Claude Code, Cursor, etc. send your prompts and the tool responses to their model provider over the network.
  • Tool responses can include class names, field values, retained-size figures, and (with --show-strings) raw heap content. Treat the redaction defaults accordingly.
  • For sensitive heap dumps from production, leave the defaults in place. The redaction gates are conservative by design. String content, byte[], char[], and short[] are summarized rather than emitted verbatim. See the next FAQ entry for details.
  • For extra-sensitive dumps (financial, medical, regulated data), also pass --show-primitives=false. Scalar primitive values and numeric array contents (int[], long[], …) are shown by default and may leak IDs, timestamps, or numeric payloads that the string gate does not cover.
  • HTTP mode has no authentication. The default bind is localhost. --root limits which files the server is willing to load and --allowed-origin restricts the CORS Origin header, but neither is a substitute for authentication on a network-exposed port.
?
What do --show-strings, --show-secrets, and --show-primitives change?
▼

Two independent gates control what reaches the AI. Both are ON by default.

  • Injection gate: prevents raw heap strings from reaching the model context. String/StringBuilder/StringBuffer and primitive byte[]/char[]/short[] arrays are replaced with a shape summary plus a closed-vocabulary format hint (json, xml, jwt, base64, …). The model sees the shape, not the content. This guards against two distinct risks: data exfiltration (a malicious prompt coaxing the model into echoing PII, tokens, or session content) and context poisoning (heap-resident strings carrying attacker-controlled prose, e.g. logged HTTP request bodies or user input, that could steer the model into ignoring instructions, calling tools maliciously, or hallucinating). A raw heap is an untrusted input channel even when the application is friendly. --show-strings disables this gate; only use with fully trusted dumps.
  • Sensitive-name gate: applies a second pass that redacts field and map values whose name matches a credential pattern (password, token, secret, key, …). --show-secrets disables this.
  • --show-primitives=false closes a third channel that the injection gate does not cover: scalar primitive field values (int, long, boolean, char, …) and all primitive arrays (int[], long[], float[], …). Shown by default. Disable for extra-sensitive dumps where numeric IDs, timestamps, or array contents must never be surfaced.
?
Why does deepheap require JDK 25+?
▼
  • deepheap is written using Java language features introduced in JDK 25, so the runtime needs to be JDK 25 or later.
  • The heap dumps themselves can come from any JVM version; deepheap parses HPROF files from JDK 8 onward.
  • If JDK 25 isn't your default JVM, you can install it side-by-side (e.g. via Adoptium Temurin) and point only deepheap at it; the rest of your toolchain is unaffected.
?
How do I update deepheap?
▼
  • Download the new zip from the Downloads page and unzip over the existing install (or to a new path).
  • Restart your MCP client (or stop the running server) so it launches the new binary on the next tool call.
  • If you're running HTTP mode, stop the running process and start the new one; there's no rolling update story for HTTP mode.