hprof-analyzer 0.1.0

Fast, low-memory Java HPROF heap-dump analyzer with Eclipse MAT-parity reports (System Overview, Leak Suspects, Top Consumers).
hprof-analyzer-0.1.0 is not a library.

hprof-analyzer

CI License: MIT

Your JVM died with an OutOfMemoryError and left behind a multi-gigabyte .hprof heap dump. You want to know what filled the heap without opening a 34 GB file in a GUI or provisioning a machine as big as the dump.

hprof-analyzer is a command-line tool that reads the dump and writes a self-contained report covering the same ground as Eclipse MAT's System Overview, Leak Suspects, and Top Consumers analyses, plus additional views. Peak RSS stays well below the dump size: on a 33 GiB dump it peaks at ~15 GiB where MAT needs ~62 GiB (see Performance). The report is a single file you can email, attach to a ticket, or diff in CI.

An experimental tool by the SapMachine team.

What you get

Run one command and get a report with these sections:

  • System Overview: tells you how large the heap is, which classes and class loaders dominate it, how many duplicate class definitions exist, and which GC roots are anchoring memory. Includes a per-class histogram with a "largest single instance" column so you can spot the one object inflating a count.
  • Leak Suspects: identifies the objects retaining the most memory and traces each one back to its GC root, so you can see the exact reference chain keeping it alive — the same root-path view MAT shows in its Leak Suspects report.
  • Top Consumers: ranks classes, class loaders, and packages by retained size (not just shallow size), so allocations hidden inside containers show up under the right owner.
  • Threads: maps each thread's stack frames to the local variables they keep alive, so thread-local accumulation is visible without a GUI.
  • Duplicate strings (opt-in, --dup-strings): quantifies wasted memory from identical java.lang.String values — how many bytes are duplicated, the top 25 offending values, a string-length histogram, and the top 25 classes holding the most String references.
  • Collections analysis (opt-in, --collections): deep inspection of every Map, List, Set, and array in the heap. Shows fill ratios (how full backing arrays are), size distributions, map collision rates, the biggest individual collections, constant primitive arrays that could be deduplicated, and container attribution by Class#field — which field on which class holds the most elements and retains the most memory across all live instances. Supports standard JDK collections, Kotlin collections, and Eclipse Collections out of the box; custom types can be registered via a TOML config file.

Pick the format that fits: plain Markdown, Markdown with ASCII graphs (bars, sparklines, dominator trees), a self-contained HTML page you can open in any browser, or machine-readable JSON.

A live viewer shows all four output formats side by side, built from the public Renaissance benchmark scala-doku dump:

Open the sample report viewer

Switch formats in the top-left; the Default / All features toggle beside them swaps between a default-options run and one with every optional analysis on (--dup-strings --collections). On either Markdown view, hit Render to HTML in the top-right to see it formatted. The raw files are here:

Format Default options All optional features
Plain Markdown scala-doku.md scala-doku-full.md
Markdown with ASCII graphs scala-doku.graphs.md scala-doku-full.graphs.md
Self-contained HTML (opens live) scala-doku.html scala-doku-full.html
Machine-readable JSON scala-doku.json scala-doku-full.json

Quick start

Grab a prebuilt binary and analyze a dump in two commands. No Rust, no Node, no build step. Pick the line for your platform (see Install for all targets and other install methods):

# macOS (Apple Silicon)
curl -L https://github.com/parttimenerd/hprof-analyzer/releases/download/nightly/hprof-analyzer-aarch64-apple-darwin.tar.gz | tar xz

# Linux (x86_64, glibc)
curl -L https://github.com/parttimenerd/hprof-analyzer/releases/download/nightly/hprof-analyzer-x86_64-unknown-linux-gnu.tar.gz | tar xz

That unpacks a folder containing the hprof-analyzer binary. Run it on your dump:

./hprof-analyzer-*/hprof-analyzer heap.hprof report.html

Open report.html in any browser. To run it from anywhere, move the binary onto your PATH (e.g. sudo mv hprof-analyzer-*/hprof-analyzer /usr/local/bin/), then just hprof-analyzer heap.hprof report.html. The rest of this README assumes it is on your PATH.

There is no subcommand to remember. Hand the tool a .hprof dump and it analyzes it; hand it a saved report JSON and it re-renders it to any format without re-parsing the dump. With an output path, the format is inferred from the extension; without one, it prints Markdown to stdout.

hprof-analyzer heap.hprof report.html    # write an HTML report
hprof-analyzer heap.hprof                 # or Markdown to stdout

Analysis time scales with the dump. A small dump is done in seconds; a multi-gigabyte dump takes minutes (see Performance). Gzip-compressed dumps (.hprof.gz) are read transparently.

Why you might want it

  • Very large dumps at bounded memory. Two-pass streaming keeps peak RSS far below the dump size. A 33.4 GiB dump (~7.5 GiB gzip-compressed, ~20 GiB live heap) analyzes in ~13.5 minutes at ~14.7 GiB peak RSS, where MAT needs ~62 GiB (see Performance). No heap-size flag to tune.
  • Scriptable and CI-friendly. Never prompts, never opens a window. Emit JSON, diff two dumps to catch memory growth in a pipeline, or gate a build on retained-size regressions.
  • Emailable output. The HTML report is a single self-contained file with no server and no external assets — attach it to a ticket or share it as-is.
  • Deterministic. Markdown output is byte-stable (modulo the generation timestamp), so it diffs cleanly across runs and across dumps.

When to use alternatives

This tool is deliberately narrow: it renders static replicas of the three views above plus threads, and nothing else. If you need to explore a heap — run OQL queries, walk the dominator tree interactively, inspect arbitrary objects and their fields, or use the full breadth of MAT's analyses — reach for Eclipse MAT, the complete interactive GUI. Use hprof-analyzer instead when you already know you want those reports and want them fast, scriptable, or on a dump too large to open comfortably.

If all you need is a class histogram, hprof-slurp is faster and lighter because it never builds the dominator tree. But that also means it cannot report retained sizes, leak suspects, root paths, or Top Consumers — the analyses hprof-analyzer exists to provide.

Install

Three ways to install, fastest first.

1. Prebuilt binary (recommended, no toolchain)

Download the archive for your platform, unpack it, and put the hprof-analyzer binary on your PATH. The archives bundle the HTML report's assets already, so you need no Rust and no Node.js.

Platform Archive
Linux x86_64 (glibc) hprof-analyzer-x86_64-unknown-linux-gnu.tar.gz
Linux x86_64 (static musl) hprof-analyzer-x86_64-unknown-linux-musl.tar.gz
Linux aarch64 (glibc) hprof-analyzer-aarch64-unknown-linux-gnu.tar.gz
Linux aarch64 (static musl) hprof-analyzer-aarch64-unknown-linux-musl.tar.gz
macOS (Apple Silicon) hprof-analyzer-aarch64-apple-darwin.tar.gz
Windows x86_64 hprof-analyzer-x86_64-pc-windows-msvc.zip

A rolling nightly pre-release always tracks the latest commit on main. Download it in one line (swap in the archive for your platform):

curl -L https://github.com/parttimenerd/hprof-analyzer/releases/download/nightly/hprof-analyzer-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv hprof-analyzer-*/hprof-analyzer /usr/local/bin/
hprof-analyzer --help

Once a versioned release is tagged, the same archives are also published there and reachable via the stable .../releases/latest/download/<archive> URL; until then, use the nightly URL above. You can also browse every asset on the Releases page.

The static musl build has no libc dependency, so it runs on any Linux (including minimal containers and older distros). Prefer it if the glibc build complains about a missing or too-old libc.

2. With Cargo

Needs a Rust toolchain (1.85+, edition 2024). Install rustup and run rustup update stable if your toolchain is older, then from a checkout of this repo:

cargo install --path .

This installs hprof-analyzer into ~/.cargo/bin (ensure it is on your PATH). Node.js/npm is optional: the repository ships a pre-built web/dist/bundle.js, so the build works without it. If you modify the web sources under web/src/, npm is needed to regenerate the bundle (see Building and testing).

3. From source

Same toolchain requirements as option 2 (Rust 1.85+; Node.js/npm only needed if you change the web sources):

git clone https://github.com/parttimenerd/hprof-analyzer
cd hprof-analyzer
cargo build --release
# binary at target/release/hprof-analyzer

Usage

hprof-analyzer <INPUT> [OUTPUT] [OPTIONS]

Give it a path and it does the right thing:

  <INPUT>   a .hprof[.gz] heap dump  → analyze it and write a report
            a saved report .json[.gz] → re-render it to another format

Named subcommands:
  compare      Compare reports (MAT export vs ours, or two of ours across time)
  completions  Generate a shell completion script
  dev          Developer / diagnostic commands

Analyze a dump

Output goes to stdout, or to a file if you pass one. When you give an output file and no -f, the format is inferred from its extension (.html → HTML, .json / .json.gz → JSON, .md → Markdown); -f always wins. Stdout defaults to plain Markdown.

hprof-analyzer heap.hprof                    # plain Markdown to stdout
hprof-analyzer heap.hprof report.html        # HTML (inferred from .html)
hprof-analyzer heap.hprof report.json        # JSON (inferred from .json)
hprof-analyzer heap.hprof report.json.gz     # gzip-compressed JSON
hprof-analyzer heap.hprof -f md-graphs       # Markdown with ASCII graphs

md-graphs shares the .md extension with plain Markdown, so it is never inferred; ask for it explicitly with -f md-graphs.

Duplicate strings. Add --dup-strings to include the duplicate-String section (see What you get). It adds two extra scans of the heap file, so it is off by default:

hprof-analyzer heap.hprof report.html --dup-strings

Progress. Long runs on multi-GB dumps print a live phase line to stderr when stderr is a terminal. Control it with --progress auto|always|never (default auto; auto stays silent when stderr is piped or when --verbose / --trace-rss are already printing phases).

Tune the report size with --detail

Every report includes four deeper analyses, always on:

  • Root paths: dominator chain from each single-object suspect up to its GC root (MAT-style).
  • Allocation sites: objects aggregated by allocation stack-trace serial.
  • Thread locals: each thread's local root objects.
  • Dominator subtree: the multi-level dominator subtree per accumulation point.

One flag scales the output-size caps for these sections (and the top-consumer / leak-suspect lists):

hprof-analyzer heap.hprof --detail minimal   # smaller report, tighter caps
hprof-analyzer heap.hprof --detail default   # the default
hprof-analyzer heap.hprof --detail max       # larger report, looser caps

The preset controls seven caps:

--detail root depth alloc top thread locals dom nodes dom depth leak children top consumers
minimal 10 15 5 500 10 15 10
default 30 50 20 5,000 20 50 20
max 200 500 100 100,000 50 500 100

Two caveats. Memory: --detail max can raise the dominator-tree cap to 100k nodes and push peak RSS higher on very large dumps; that is the documented tradeoff. Allocation tracking: allocation sites only yield real stacks if the JVM recorded allocation stack traces (stack_trace_serial); most HotSpot dumps have this off, and the report says so rather than inventing data.

Compare against a MAT export

Compare a MAT System Overview HTML export against our canonical JSON; exits non-zero on a parity failure (useful as a test gate):

hprof-analyzer heap.hprof report.json
hprof-analyzer compare mat mat_System_Overview.zip report.json

Track growth across two dumps

Compare an earlier report against a later one to see what grew. This is a handy way to find a leak by comparing snapshots over time:

hprof-analyzer early.hprof a.json
hprof-analyzer later.hprof b.json
hprof-analyzer compare reports a.json b.json

Re-render a saved report

The JSON is the canonical form; re-render it to any format without re-parsing the dump. Just pass the report path as the input, and the tool sees it is a saved report, not a dump, and re-renders it. It takes an optional output path with the same extension inference:

hprof-analyzer report.json                    # Markdown to stdout
hprof-analyzer report.json report.html        # HTML (inferred from .html)
hprof-analyzer report.json -f md-graphs       # Markdown with ASCII graphs

The analyze-only flags (--dup-strings, --collections, non-default --detail) have no effect when re-rendering, because those sections are baked into the JSON at analyze time, so passing one on a report input is an error with a hint to re-run on the .hprof dump.

Compressed JSON

Write the canonical report gzip-compressed by giving the output path a .gz suffix (the JSON is repetitive and typically shrinks ~20×). A .gz report reads back transparently, because the tool sniffs the gzip magic bytes, so it also works from stdin:

hprof-analyzer heap.hprof report.json.gz    # gzip-compressed JSON (inferred)
hprof-analyzer report.json.gz -f md-graphs  # read it back, no manual gunzip

Shell completions

Generate a completion script for your shell and install it where the shell looks for completions:

hprof-analyzer completions zsh  > ~/.zsh/completions/_hprof-analyzer
hprof-analyzer completions bash > /etc/bash_completion.d/hprof-analyzer

Performance

Three representative workloads are measured below: a large real-world dump (resource numbers shared but the dump itself is not), a HeapothesYs HyperAlloc synthetic allocation dump (~10 GiB file), and a VS Code / Eclipse-based JVM dump (~1 GiB file). The latter two are reproducible public dumps you can regenerate. All sizes are in binary units (GiB/MiB), and wall-clock times are minutes:seconds. Each row records the exact commit so the numbers stay meaningful as the tool evolves.

All rows were measured on an AMD Ryzen Threadripper PRO 3995WX (64 cores / 128 threads) with 123 GiB RAM. The "ours" columns are hprof-analyzer; the "MAT" columns are Eclipse MAT 1.17.0 on the same dump, for comparison.

Workload Heap (live) Dump file RSS (ours) RSS (MAT) Wall (ours) Wall (MAT) Measured
Large real-world dump ~20 GiB 33.4 GiB (~7.5 GiB gzip) 14.65 GiB 62.05 GiB 13:21 27:16 2026-07-19, 86006f7
HeapothesYs HyperAlloc 7.91 GiB 10.32 GiB 0.94 GiB 20.32 GiB 1:20 1:48 2026-07-19, 86006f7
VS Code JVM 0.73 GiB 1.01 GiB 0.49 GiB 5.27 GiB 0:22 1:27 2026-07-19, 86006f7

MAT was run with ParseHeapDump.sh (leak-suspects + top-components) with its MemoryAnalyzer.ini configured for -Xmx60g; peak RSS scales with the dump (62 GiB on the largest). hprof-analyzer holds peak RSS far below the dump size and needs no heap tuning. Correctness is validated against MAT 1.17.0: the compare mat subcommand diffs a MAT System Overview export against our JSON, and the parity fixtures gate on it (see Compare against a MAT export).

Peak RSS stays well below both the dump-file size and the live-heap size (~40% of the uncompressed file on the big run, and a fraction of that on the smaller ones) because the analyzer never holds the whole dump in memory; it streams the records in two passes and works over compressed, bounded-size index structures.

How it works

The two-pass parser, the dominator-tree construction, the shallow/retained size formulas, and the compressed index structures are described in DESIGN.md.

Contributing

Contributions are welcome. See DESIGN.md for the two-pass parser, dominator-tree construction, size formulas, and index structures — the context you need to extend the analyzer.

Building and testing

Requires a stable Rust toolchain (1.85+, edition 2024); see Install. All commands run from the repository root:

cargo build --release        # optimized binary at target/release/hprof-analyzer
cargo test --release         # unit tests + JSON-schema + report parity fixtures
cargo fmt --all -- --check   # formatting gate (matches CI)
cargo clippy --release --all-targets -- -D warnings   # lint gate (matches CI)

CI (.github/workflows/ci.yml) runs the same fmt, clippy -D warnings, and test steps on stable. The parity fixtures the tests read live under tests/fixtures/ (checked in alongside the tests).

The self-contained HTML report embeds a small React bundle (web/dist/bundle.js). This bundle is committed to the repository so that cargo install and crates.io builds work without Node.js. When you change the web sources under web/src/, build.rs re-bundles automatically on the next cargo build if npm is on your PATH; you can also rebuild it by hand:

cd web && npm install && npm run build   # regenerates web/dist/bundle.js

If npm is absent, the committed bundle is used as-is. If you only need the binary and want to avoid the Node toolchain entirely, download a prebuilt release binary instead (see Install).

Support & Feedback

Bug reports, feature requests, and contributions are welcome via GitHub issues.

License

MIT. See LICENSE.

Copyright 2026 SAP SE or an SAP affiliate company, Johannes Bechberger and contributors.