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:
- GUI-oriented: headless batch reports exist (e.g. MAT's
ParseHeapDump.sh) but the interactive query experience lives in a desktop UI, and the output formats are designed for human review rather than machine consumption. - Memory-hungry on large dumps: when the in-memory object graph approaches or exceeds available RAM, parse time and stability degrade; tuning the analyzer's own heap is often required.
- Query language barrier: OQL and similar query languages are powerful but require knowledge of query syntax and HPROF internals to be useful.
- Separate context: findings live in a separate GUI window; copying relevant data to an AI conversation requires manual effort.
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:
- Local execution: deepheap runs entirely on your machine. No network calls, no telemetry, no upload of the dump.
- Structured results: tool calls return class counts, retained sizes, GC root paths, and field values as deterministic facts. The AI synthesizes an answer from those facts.
- Filtered content: raw heap strings, byte[], and char[] are summarized before they reach the AI client (see the two gates below).
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.
- 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
Request a license and download
Request a trial license and grab
deepheap-<version>.zipfrom the Downloads page. The license file is emailed to you and is required to run the server. -
2
Unzip to a permanent location
unzip deepheap-<version>.zip -d ~/tools/deepheap -
3
Verify the installation
~/tools/deepheap/mcp/bin/mcp --helpYou should see the usage text printed to stdout.
-
4
Connect your MCP client
See MCP setup below for client-specific configuration.
-
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.SessionCachestill 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) 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:
Global config
To make deepheap available in every project, add the same block to ~/.claude/mcp.json.
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.
Start with the big picture, then narrow down to the leaking objects and trace who's keeping them alive.
- 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).
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.
java_frameroot: 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_globalroot: a native library is holding it. Common with JDBC drivers, JNI integrations, off-heap caches.sticky_classor 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, orThreadLocalis usually the actual leak surface.
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.
- 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, OSGiBundle, plugin manager) holding a reference it should have released. ThreadLocalvalues rooted in pooled threads are a frequent culprit. The thread outlives the redeploy and pins the old loader.
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.
- 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_eventsbefore 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."
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.
- 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
-
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_pathto 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_pathto 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 toheap_dump__read.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
-
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
-
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.
- 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.
- Double-check the absolute path to the
mcpbinary 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 --helpshould 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.
- 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__readcall 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.
Use jcmd on the running JVM. Find the PID with jps, then:
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:
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.
Use jcmd on the running JVM. Find the PID with jps, then:
For a plain-text thread dump:
For the JSON format (JEP 425, JDK 21+), which is the only one that scales to millions of virtual threads:
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.
On JDK 9+, use the unified logging framework. Add this to the JVM command line:
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;%texpands to a timestamp so restarts don't overwrite. Usefile=/var/log/myapp/gc-%t.logfor 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.
- Heap dumps: HotSpot HPROF binary only, what you get from
jmap -dump:format=b,jcmd <pid> GC.heap_dump, or-XX:+HeapDumpOnOutOfMemoryErroron 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__readreturns after loading. - Thread dumps: jstack /
jcmd Thread.printtext 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.
- 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
--rootand--allowed-originto lock down what the server is willing to load and which origins can talk to it.
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-stringsdisables 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-secretsdisables this. --show-primitives=falsecloses 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.
- 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.
- 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.