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 by calling tools. 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?"

It's a headless replacement for Eclipse MAT and VisualVM. There's 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 (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?

What existing tools do well, where they fall short for AI-augmented workflows, and the architectural choices that address the gaps.

The problem with Eclipse MAT and VisualVM

These tools are excellent for interactive GUI analysis, but they hit friction in several scenarios:

Why MCP over OQL?

OQL is powerful but requires the analyst to know what they're looking for and how to express it. 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?

By default, 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, which queries the dump and returns structured tool 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[] and char[], regardless of whether it backs a String, is redacted to byte[N] (REDACTED, format=uri). The format label comes from a closed vocabulary of about 20 values (json-object, json-array, uri, jwt, pem, base64, xml, sql, uuid, iso-date, …), so an attacker controls at most 4 bits of LLM input per string. 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

Short definitions of the terms used throughout this page. Skim once, refer back when something looks unfamiliar.

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 on demand.

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 (obtained from the Downloads page)
  1. 1

    Request a license and download

    Request a trial license and grab deepheap-<version>.zip from the Downloads page. The license file is emailed to you and is required to run the server.

  2. 2

    Unzip to a permanent location

    unzip deepheap-<version>.zip -d ~/tools/deepheap
  3. 3

    Verify the installation

    ~/tools/deepheap/mcp/bin/mcp --help

    You should see the usage text printed to stdout.

  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 deepheap is wired up, the AI client does the work. You ask questions in natural language and it picks the right tools. Here's what a first session might look like.

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 very 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 analysis stays resident.

MCP setup

deepheap speaks the Model Context Protocol over stdio (default) or HTTP (Streamable HTTP). Pick your client below.

Most users should start with stdio mode. Your client launches the server as a local subprocess. HTTP mode is for shared or remote deployments where multiple clients connect to one running server.

Project config (recommended)

Create or edit .mcp.json in your project root:

// .mcp.json { "mcpServers": { "deepheap": { "type": "stdio", "command": "/path/to/deepheap/mcp/bin/mcp" } } }

Global config

To make deepheap available in every project, add the same block to ~/.claude/mcp.json.

Claude Code auto-restarts the MCP server on the next tool call if the process exits; no manual restart needed after updates.

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 just 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 (each GC frees less than the last) 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.

Heap dump

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. Accepts an optional mapping_path to apply a ProGuard/R8 mapping and permanently deobfuscate class and field names on load.
  • heap_dump__reconfigure_snapshot Change JVM layout settings (compressed OOPs, compact headers) and reanalyze without re-reading the file. Also accepts mapping_path to apply a ProGuard/R8 deobfuscation mapping if one was not supplied at load time. 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.
Overview & Triage
  • heap_dump__get_classes All classes ranked by instance count, shallow size, or retained size. Supports name filters, regex, and package grouping.
  • heap_dump__get_class_hierarchy Show the superclass chain and subclasses for any class, with instance count and retained size per level.
Object Inspection
  • heap_dump__get_instances Find instances of a class (substring match), sorted by retained size. Supports field value filters and subclass inclusion.
  • heap_dump__get_object_detail All fields, types, and values for a single object. Class objects also show static fields.
  • heap_dump__get_object_contents Render a collection's actual contents: entries of ArrayList, HashMap, arrays, StringBuilder, and Scala / Kotlin / Groovy collection types. Use when: you've found a suspect cache, queue, or map and want to see what's inside rather than just its size. Works on the standard JDK collections plus the major JVM languages' own types.
  • heap_dump__get_inbound_references Who holds a reference to a given object, sorted by the referrer's retained size.
Retention & Reachability
  • heap_dump__get_gc_roots All GC root objects grouped by root type (JNI global, thread frame, static field, monitor, …).
  • heap_dump__get_gc_root_path The exact reference chain from a GC root to a target object. Answers "why is this still alive?"
  • heap_dump__get_dominator_tree_node Navigate the dominator tree. For any node, list the objects it exclusively retains, sorted by retained size. 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.
Native Memory & System Resources
  • heap_dump__get_off_heap_memory All DirectByteBuffer, NativeMemorySegment, and MappedMemorySegment allocations with sizes and addresses.
  • heap_dump__get_open_files Open file descriptors with path and access mode at heap dump time.
  • heap_dump__get_network_connections TCP/UDP sockets, DNS configuration, JDWP port, and HTTP client presence.
  • heap_dump__get_threads All threads with stack traces and retained sizes, sortable by retained size or name.
ClassLoaders & Duplicates
  • heap_dump__get_class_loaders All ClassLoaders with their JAR/URL paths and associated Java module instances.
  • heap_dump__get_duplicate_classes Classes loaded by more than one ClassLoader. A common cause of ClassCastException in OSGi and plugin frameworks.
  • heap_dump__get_duplicate_strings String instances with identical content backed by separate char arrays, wasting heap. Sorted by wasted bytes.
System Metadata
  • heap_dump__get_system_properties JVM system properties as key=value pairs (sensitive values redacted by default).
  • heap_dump__get_system_environment OS environment variables visible to the JVM process.
  • heap_dump__get_jfr_recordings Active JFR (Java Flight Recorder) recordings, their state, and event settings.
  • heap_dump__get_java_agents Attached Java agents loaded via -javaagent, -agentpath, or -agentlib flags.
  • heap_dump__get_jmx_mbeans JMX MBean attributes (primitives, Strings, AtomicLong/Integer), filterable by domain or object name.
  • heap_dump__get_modules Java modules (JPMS) loaded by the JVM, with module names, versions, and the ClassLoader that defines each module.

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.
  • 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).

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 unified (JDK 9+) and pre-unified (JDK 8) formats; supports G1, Parallel, Serial, CMS, ZGC, and Shenandoah. 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.
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.
  • 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.
  • gc_log__get_humongous_events Top-N G1 humongous-allocation events sorted by regions assigned, with cause, timestamp, pause duration, and heap before/after. G1 only. Use when the TL;DR flags frequent humongous allocations.
  • 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.

FAQ & troubleshooting

Answers to the questions that come up most often during setup, debugging, and updates.

?
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/mcp/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 and never loads the full file into RAM. A 100 GB dump should work on a 16 GB laptop. If it doesn't, 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 graph stays resident 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. IBM J9 / OpenJ9 PHD dumps and GraalVM dumps are not supported.
  • 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, ZGC, Shenandoah.
?
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. That's how the AI side of the workflow works.
  • 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[], and char[] 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 adds --root and --allowed-origin to lock down what the server is willing to load and which origins can talk to it.
?
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[] 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 or curious prompt coaxing the model into echoing PII, tokens, or session content back to the user or upstream provider) 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).
  • On the next MCP tool call, your client launches the new binary automatically. No process management needed.
  • If you're running HTTP mode, stop the running process and start the new one; there's no rolling update story for HTTP mode.