jvm tools
A collection of tools and libraries for diagnosing, profiling, and understanding running Java applications. Built at SAP as part of the SapMachine team, open-sourced for the broader JVM community.
Live JVM Inspection
Instant JVM insight — deadlocks, hot threads, flamegraphs — no agent needed.
You need instant insight into a running JVM without attaching a full profiler
DetailsJFR / Profiling
Fork of Firefox Profiler that opens JFR and .cjfr recordings natively in the browser — drag-and-drop, no server.
You want to open a JFR or condensed-data (.cjfr) recording directly in the browser without running a local server
DetailsSQL notebook for JFR recordings and JDK GC logs — drag in a file, write SQL, get charts.
You want to run ad-hoc SQL queries over JFR event data or JDK unified log files (GC logs)
DetailsCLI tool and agent for compact long-term JFR event storage — significantly smaller than raw .jfr.
You need long-term storage of JFR recordings with minimal disk usage
DetailsReference site for every JFR event type across JDK versions.
You need to know what fields a JFR event has across JDK versions
DetailsEducational Java CPU profiler — method table and HTML flame graph, no native code.
You want to understand how a sampling profiler works from first principles
DetailsHeap Dump Analysis
Fast heap dump analyser — MAT-parity reports, OQL, MCP server, redaction, browser UI.
You need to analyse a heap dump without Eclipse MAT's memory overhead
DetailsCrash / Log Diagnostics
Parse, transform, and redact HotSpot hs_err crash files — typed model, redaction, CLI.
You need to parse hs_err files programmatically and access threads, stack frames, registers, or VM info
DetailsInteractive browser tool to explore -Xlog configs — see which statements match before deploying.
Config search with autocomplete over all -Xlog tags and levels
DetailsTesting
JAR Packaging
Debugging
Agent Development
Libraries
Learning Resources
Companion code for "Writing a Profiler from Scratch" — minimal JVMTI/AsyncGetCallTrace profiler.
You are reading the blog series and want to run the example code
DetailsOne-shot CLI inspector for running JVMs. Detects deadlocks, identifies hot threads, captures short-burst flamegraphs via async-profiler, and provides a live TUI — all without a persistent agent or IDE attachment. Use it when you need instant insight into a misbehaving JVM in production, staging, or CF without setting up a full profiler.
When to use
- You need instant insight into a running JVM without attaching a full profiler
- You need deadlock detection, hot thread analysis, or a quick flamegraph
- You are working in a Cloud Foundry Java environment
- You want a live TUI showing thread states, GC activity, and memory in real time
When not to use
- You need continuous long-term profiling (use condensed-data + JFR instead)
- You need allocation or lock profiling in depth (use async-profiler directly)
- You are on Windows (the native binary is Linux/macOS only)
Related
Install
curl -L https://github.com/parttimenerd/jstall/releases/latest/download/jstall.jar \
-o jstall.jar
java -jar jstall.jar --help
Usage
java -jar jstall.jar status <pid>
java -jar jstall.jar flame <pid>
Find more information at https://github.com/parttimenerd/jstall
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Get a quick report on hottest methods and active threads
Run the default status command — it combines deadlock detection, hot thread
identification, thread categorization, and a dependency graph in one shot:
java -jar jstall.jar status <pid>
# Or match by JVM name instead of PID
java -jar jstall.jar status MyApplication
Use --intelligent-filter to collapse framework internals and focus on
application code, or --full for the unfiltered output.
Find out where your JVM is stuck or not responding
Run deadlock first — if there is a deadlock it is the root cause and everything
else is a symptom:
java -jar jstall.jar deadlock <pid>
If no deadlock, check which threads are BLOCKED or WAITING and what they are waiting for:
java -jar jstall.jar status <pid> --intelligent-filter
Look for threads in BLOCKED state and the monitor address they are waiting on.
The thread currently holding that monitor is the bottleneck.
For a continuous view while the JVM is unresponsive, use the live TUI:
java -jar jstall.jar threads --live <pid>
Find out why CPU is high
most-work takes repeated thread dumps and ranks threads by on-CPU frequency —
no async-profiler needed, works on any JVM:
java -jar jstall.jar most-work --dumps 5 <pid>
# Show only top 3 threads
java -jar jstall.jar most-work --dumps 5 --top=3 <pid>
For a proper CPU flamegraph (requires async-profiler, bundled on Linux/macOS):
java -jar jstall.jar flame --duration=30 --open <pid>
Check if memory is growing without taking a full heap dump
The status command includes a heap histogram showing the top object types by
instance count and retained bytes — much faster than a full jmap -histo:
java -jar jstall.jar status <pid> | grep -A 20 "Heap"
Take two snapshots a minute apart and compare to see which class is accumulating:
java -jar jstall.jar record <pid> --output before.zip
sleep 60
java -jar jstall.jar record <pid> --output after.zip
java -jar jstall.jar status before.zip
java -jar jstall.jar status after.zip
If the leak is confirmed, trigger a full heap dump for analysis with hprof-analyzer:
jcmd <pid> GC.heap_dump heap.hprof
Find the threads doing the most CPU work
java -jar jstall.jar most-work <pid>
# Take 3 dumps for a more accurate picture
java -jar jstall.jar most-work --dumps 3 <pid>
# Show only the top 5 threads
java -jar jstall.jar most-work --top=5 <pid>
most-work takes multiple thread dumps and ranks threads by how often they
appear on-CPU across samples.
Capture a flamegraph
java -jar jstall.jar flame <pid>
# Record for 30 seconds
java -jar jstall.jar flame --duration=30 <pid>
# Open in browser automatically
java -jar jstall.jar flame --open <pid>
Runs async-profiler for a short burst and writes an interactive HTML flamegraph. Requires async-profiler on the same host (bundled for Linux/macOS).
Record the current state to a zip for offline analysis or sharing
java -jar jstall.jar record <pid> --output diagnostics.zip
# Replay later
java -jar jstall.jar status diagnostics.zip
java -jar jstall.jar most-work diagnostics.zip
The zip contains thread dumps, heap histogram, GC log excerpt, and other
diagnostic data. Use -f diagnostics.zip as a global flag to replay any command.
Inspect a Cloud Foundry app
The cf-cli-java-plugin embeds jstall and exposes it via cf java jstall.
You can also use jstall directly with the --cf option:
# Via cf-cli-java-plugin
cf java jstall $APP_NAME
cf java jstall $APP_NAME --args 'deadlock all'
cf java jstall $APP_NAME --args 'most-work --dumps 3 all'
cf java jstall $APP_NAME --args 'flame all'
# Or directly with jstall
java -jar jstall.jar --cf $APP_NAME status all
Latest release notes
- `vm-vitals`: header rows and data columns now always align correctly (fixes garbled output on SapMachine 3-tier CF format)
- `vm-vitals`: ~5 s startup delay eliminated, first result now appears immediately
- `compiler-queue`, `gc-heap-info`, `jvm-support`, `vm-classloader-stats`, `vm-metaspace`: faster startup as these commands no longer collect unnecessary thread dumps
- Dependency: `ap-loader-all` bumped `4.5-13` → `4.6-14`
- `jstall.jar` - Executable JAR file (requires Java 21+)
- `jstall` - Standalone launcher script (Unix/Linux/macOS)
- `jstall-minimal.jar` - Minimal executable JAR (Sa…
Flamegraph capture requires async-profiler on the same host. The JAR bundles a copy for Linux/macOS; on other platforms provide it via --async-profiler-path.
Cloud Foundry CLI plugin to troubleshoot Java apps running on CF without SSH. Trigger heap dumps, thread dumps, and async-profiler or JFR recordings from the cf command line, with results streamed back to your machine. Heap dumps can be redacted during download, kept compressed, or opened directly in hprof-analyzer. Also embeds jstall for full JVM inspection via `cf java jstall`.
Features
- map[body:Capture heap dumps and thread dumps from Cloud Foundry apps without manual SSH sessions. title:Stream heap dumps directly to your machine]
- map[body:Use `--redact` or `--redact-complete` to zero sensitive primitive data while the dump is being streamed, so an unredacted heap dump is never written locally. title:Redact sensitive heap-dump data during download]
- map[body:Use `--compress` to save `.hprof.gz` files directly, or `--open` / `--open-url` to inspect the heap dump in hprof-analyzer right after download. title:Keep dumps compressed or open them immediately]
- map[body:Inspect deadlocks, hot threads, flame graphs, and more via `cf java jstall` without separately installing jstall. title:Run bundled jstall diagnostics]
When to use
- You run Java apps on Cloud Foundry and need heap dumps, thread dumps, or CPU profiles
- You want jstall-style JVM inspection without SSH access to the container
- You need to record JVM diagnostic data (status zip) for offline analysis
When not to use
- You are not using Cloud Foundry (use jstall directly for non-CF JVMs)
- You need real-time continuous profiling rather than one-shot diagnostics
Related
Install
# Pick the binary for your platform:
cf install-plugin https://github.com/SAP/cf-cli-java-plugin/releases/latest/download/cf-cli-java-plugin-macos-arm64
# linux-amd64 / linux-arm64 / windows-amd64 / windows-arm64 also available
# macOS requires Apple Silicon; macOS Intel (darwin/amd64) is not supported
Usage
cf java heap-dump my-app
cf java heap-dump my-app --redact --open
cf java heap-dump my-app --redact --compress
cf java thread-dump my-app
cf java jstall my-app
Find more information at https://github.com/SAP/cf-cli-java-plugin
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.My CF app is not responding — find what it is stuck on
Run jstall first — it detects deadlocks, identifies BLOCKED threads, and shows what
each thread is waiting for:
cf java jstall $APP_NAME
If there is a deadlock, it will be listed at the top with the cycle of threads and monitors. For a focused deadlock check only:
cf java jstall $APP_NAME --args 'deadlock all'
If no deadlock, look for threads in BLOCKED state and the monitor they are waiting on.
The thread that holds that monitor is the bottleneck. For a plain thread dump:
cf java thread-dump $APP_NAME
My CF app is using too much CPU
most-work takes repeated thread dumps and ranks threads by on-CPU frequency —
no async-profiler needed:
cf java jstall $APP_NAME --args 'most-work --dumps 5 all'
For a proper CPU flame graph (slower, but much more detail):
cf java jstall $APP_NAME --args 'flame all'
# Downloads an HTML flamegraph to your current directory
Or use the two-step async-profiler approach if you want to capture during a specific window:
cf java asprof-start-cpu $APP_NAME
# reproduce the slow operation or wait 30–60 s
cf java asprof-stop $APP_NAME
# Downloads $APP_NAME-asprof-<random>.jfr — open in JDK Mission Control
My CF app crashed with OutOfMemoryError — take a heap dump
Take a heap dump from the running (or restarted) instance and download it:
cf java heap-dump $APP_NAME
Analyse with hprof-analyzer for Leak Suspects and Top Consumers:
cf java heap-dump $APP_NAME --open
# Opens hprof-analyzer in the browser with the dump pre-loaded
Or download and open manually:
hprof-analyzer $APP_NAME-heapdump-*.hprof report.html
# Open report.html → "Leak Suspects" and "Top Consumers" tabs
Note: requires jmap, which is not bundled by default in the CF Java Buildpack.
Add a full JDK via JBP_CONFIG_OPEN_JDK_JRE: '[jre: {version: 21.+}, jdk: {include: true}]'
to your app’s environment if you see a “jmap not found” error.
Take a heap dump from a running CF app
cf java heap-dump $APP_NAME
Downloads $APP_NAME-heapdump-<random>.hprof to your current directory.
Open it in VisualVM, Eclipse MAT, IntelliJ’s heap analyzer, or directly in the browser:
cf java heap-dump $APP_NAME --open
# Spins up a local server and opens hprof-analyzer in the browser automatically.
# On macOS with the Application Firewall enabled, click Allow when prompted.
# In the browsers, allow the page to access local services when prompted.
To remove sensitive data (passwords, tokens) before saving:
cf java heap-dump $APP_NAME --redact # lean: zeros primitive arrays
cf java heap-dump $APP_NAME --redact-complete # complete: zeros all primitive values
To keep the local file compressed as .hprof.gz (transfer is already gzip-compressed on JDK 17+):
cf java heap-dump $APP_NAME --compress # saves as .hprof.gz
Note: requires jmap, which is not bundled by default in the CF Java Buildpack.
Add a full JDK via JBP_CONFIG_OPEN_JDK_JRE: '[jre: {version: 21.+}, jdk: {include: true}]'
to your app’s environment if you see a “jmap not found” error.
Get a thread dump and spot deadlocks
cf java thread-dump $APP_NAME
Prints the full thread dump to stdout. To save it:
cf java thread-dump $APP_NAME > thread-dump.txt
For a richer analysis including deadlock detection, hot threads, and lock graphs, use jstall:
cf java jstall $APP_NAME --args 'deadlock all'
Profile CPU usage with async-profiler
cf java asprof-start-cpu $APP_NAME
# reproduce the slow operation or wait 30–60 s
cf java asprof-stop $APP_NAME
# Downloads $APP_NAME-asprof-<random>.jfr
Open the .jfr file in JDK Mission Control or IntelliJ to view the flame graph.
For a one-shot flame graph without manual start/stop, use jstall:
cf java jstall $APP_NAME --args 'flame all'
Record a full diagnostic snapshot for offline analysis
# Record everything (thread dump, heap histogram, jcmd output) into a zip:
cf java record-status $APP_NAME
# Include JFR recording and flame graph (slower, larger):
cf java record-status $APP_NAME --full
# Replay the zip locally with jstall:
jstall -f $APP_NAME-status.zip status all
jstall -f $APP_NAME-status.zip threads all
Useful for sharing diagnostics with teammates or filing bug reports without giving them CF access.
Inspect a Cloud Foundry app with jstall
The plugin embeds jstall directly — no separate installation needed:
# Full status report (deadlock detection, hot threads, etc.):
cf java jstall $APP_NAME
# Run a specific jstall subcommand:
cf java jstall $APP_NAME --args 'most-work --dumps 3 all'
cf java jstall $APP_NAME --args 'flame all'
To use a newer jstall version than the one bundled in the plugin,
use jstall’s own --cf option instead:
jstall --cf $APP_NAME status all
Redact sensitive data from a heap dump
The plugin can zero out sensitive values (passwords, tokens, personal data) before saving the dump locally, using the bundled hprof-redact tool:
# Lean redaction — zeros primitive arrays (byte[], char[], etc.):
cf java heap-dump $APP_NAME --redact
# Complete redaction — zeros all primitive arrays and individual primitive fields:
cf java heap-dump $APP_NAME --redact-complete
# Redact and compress:
cf java heap-dump $APP_NAME --redact --compress
# Redact and open in browser:
cf java heap-dump $APP_NAME --redact --open
The unredacted dump is never written to disk — redaction happens in-memory during download.
Capture a safer heap dump for sharing or browser analysis
If you need to inspect a heap dump locally or share it with others, combine redaction, compression, and browser opening as needed:
# Redact sensitive values and keep the dump compressed:
cf java heap-dump $APP_NAME --redact --compress
# Redact and open immediately in hprof-analyzer:
cf java heap-dump $APP_NAME --redact --open
# Use complete redaction for maximum privacy:
cf java heap-dump $APP_NAME --redact-complete --compress
Use --redact-keep-on-error only if you explicitly want to keep a partially written redacted
file when local redaction fails.
Open a heap dump in hprof-analyzer
After downloading, the plugin can spin up a temporary local server and open hprof-analyzer in the browser automatically:
cf java heap-dump $APP_NAME --open
The server binds to 127.0.0.1 (loopback only), serves the file exactly once under a
random token URL, then shuts down automatically after the browser fetches it.
macOS: if the Application Firewall is enabled, click Allow when asked whether
cf-cli-java-plugin may accept incoming network connections.
Browsers: click Allow when the browser asks for permission to access local services.
To use a locally running hprof-analyzer instance instead of the hosted one:
cf java heap-dump $APP_NAME --open-url http://localhost:8080
Latest release notes
- Per-subcommand help: `cf java help <subcommand>` prints the command's description and the
- Bundle [jstall](https://github.com/parttimenerd/jstall) v0.8.8 (jstall-minimal.jar) for one-shot JVM
- `heap-dump --redact`: zeros primitive arrays (`byte[]`, `char[]`, etc.) in the downloaded dump before saving
- `heap-dump --redact-complete`: zeros all primitive arrays and individual primitive fields (complete redaction mode,
- `heap-dump --compress`: saves the local file as `.hprof.gz` instead of decompressing it after transfer
- Transparent compressed transfer: on JDK 17+ containers, the plugin a…
Requires cf ssh to be enabled on the app (`cf enable-ssh my-app`, then restart). The heap-dump command additionally needs jmap — see the How To entry above if it is missing.
Fork of the Firefox Profiler web app with patches that add native JFR and condensed-data (.cjfr) loading and JFR-specific UI improvements — custom marker tracks, marker-based call trees, Java syntax highlighting, and an in-browser converter (drag-and-drop a .jfr or .cjfr file, no server needed). Used as the embedded viewer in jfrtofp-server.
When to use
- You want to open a JFR or condensed-data (.cjfr) recording directly in the browser without running a local server
- You are using jfrtofp-server and want the embedded profiler viewer
- You need JFR-specific UI features (custom marker tracks, Java syntax highlighting, function table)
When not to use
- You want the upstream Firefox Profiler without JFR customisations
- You need a stable, production-ready viewer (this is an experimental fork, rebased irregularly)
Related
Used by jfrtofp-server as the embedded viewer. The jfrtofp branch is rebased onto upstream periodically; the fork is squashed into one commit on top so the diff stays minimal.
Turns JFR recordings and JDK unified log files (GC logs) into a DuckDB database and exposes them through an in-browser notebook UI. Write SQL, get charts, explore event distributions, and use built-in views for common analyses — no install needed. Use it for ad-hoc investigation when you want to correlate events across types (GC, allocations, CPU) in a single query.
When to use
- You want to run ad-hoc SQL queries over JFR event data or JDK unified log files (GC logs)
- You need charts and notebook-style exploration of recordings
- You want to correlate events across types (GC, allocations, CPU) in one query
- You need to share a JFR analysis without requiring the recipient to install anything
When not to use
- You need a stable, production-ready tool (schema may change between versions)
- You need IDE integration (use the profiler plugin instead)
- You have very large recordings — DuckDB runs in-browser via WASM, memory-constrained
Install
No stable release yet — check GitHub for build instructions.
Usage
java -jar query.jar serve myrecording.jfr
# opens http://localhost:4244
Find more information at https://parttimenerd.github.io/jfr-query/
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Explore a recording in the browser without installing anything
Visit parttimenerd.github.io/jfr-query
and drag-and-drop your .jfr file. The app runs entirely in-browser via DuckDB WASM —
no data leaves your machine.
Use New from template to open ready-made notebooks for GC analysis, heap allocation, threading, and exceptions.
Diagnose long GC pauses
Use the built-in gc-pauses view to see every pause sorted by duration:
java -jar query.jar query myrecording.jfr "gc-pauses"
Or write a SQL query to find pauses over 200 ms and correlate with allocation rate:
SELECT
start_time,
duration_ms,
cause,
heap_used_after_mb
FROM gc_pauses
WHERE duration_ms > 200
ORDER BY duration_ms DESC
In the browser UI, open New from template → GC Analysis for a pre-built notebook with pause histogram, cause breakdown, and heap-after-GC chart.
Find which code is allocating the most
JFR’s ObjectAllocationInNewTLAB / ObjectAllocationOutsideTLAB events record the
stack at allocation time. Use the allocation-by-site view:
java -jar query.jar query myrecording.jfr "allocation-by-site"
Or query the top allocating methods directly:
SELECT
stack_top,
SUM(allocation_size) AS total_bytes,
COUNT(*) AS samples
FROM object_allocations
GROUP BY stack_top
ORDER BY total_bytes DESC
LIMIT 20
Note: allocation profiling must be enabled in the JFR configuration.
Use -XX:StartFlightRecording=settings=profile or enable
jdk.ObjectAllocationInNewTLAB in a custom .jfc file.
Find the hottest methods from the command line
java -jar query.jar query myrecording.jfr "hot-methods"
hot-methods is a built-in view. List all available views and macros:
java -jar query.jar views
java -jar query.jar macros
Import once, query many times with DuckDB
# Import the recording into a persistent .db file:
java -jar query.jar import myrecording.jfr mydb.db
# Query it directly with DuckDB CLI:
duckdb mydb.db "SELECT * FROM hot_methods"
# Or open DuckDB's own browser UI:
duckdb -ui mydb.db
Useful when you want to run many queries without re-parsing the JFR file each time.
Analyse GC logs (JDK unified log format)
jfr-query also accepts JDK unified log files (the output of -Xlog:gc*:file=gc.log):
java -jar query.jar serve gc.log
# or in the browser: drag-and-drop a .log file
Built-in GC log templates cover pause analysis, heap pressure, and allocation rate. Use the New from template button and select a GC log notebook.
Start the notebook server with custom templates
java -jar query.jar serve \
--templates-dir ~/my-jfr-templates \
--port 4244 \
myrecording.jfr
Point --templates-dir at a folder of .json notebook files to share team-wide analysis
templates. The AI assistant (Gemini/OpenAI) can be enabled via environment variables —
see the docs for configuration.
Runs entirely in-browser via DuckDB WASM — no data leaves your machine. Very large recordings (>500 MB) may be slow due to browser memory limits. Stack traces are stored at 10 frames depth.
CLI tool and Java agent for long-term, low-overhead JFR event storage. Records events from a running JVM via live-attach and compresses them into the self-describing .cjfr format — significantly smaller than raw JFR. Supports rotating files, configurable compression, and a reader library for querying stored data. View .cjfr files in jfr-query or the JMC fork with native .cjfr support.
When to use
- You need long-term storage of JFR recordings with minimal disk usage
- You want live-attach to a running JVM for continuous event capture
- You are storing GC or other continuous JFR data over days or weeks
- You want a self-describing format readable without the original JDK
When not to use
- You need immediate full JFR replay compatibility without a reader library
- You need a stable format (early development, .cjfr format may change)
- You need Windows support (live-attach agent is Linux/macOS only)
Install
No stable release yet — check GitHub for build instructions.
Usage
# Attach to a running JVM (by PID) and start recording:
java -jar condensed-data.jar agent <PID> start recording.cjfr
# Condense existing JFR files to .cjfr:
java -jar condensed-data.jar condense myrecording.jfr output.cjfr
Find more information at https://parttimenerd.github.io/condensed-data/
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Record a running JVM and view the results
Three commands: attach, wait, stop. No JFR configuration needed — condensed-data uses a built-in JFR profile.
# 1. Start recording (replace 1234 with your JVM's PID, or use the main class name)
java -jar condensed-data.jar agent 1234 start recording.cjfr
# 2. Let it run for as long as you need, then stop
java -jar condensed-data.jar agent 1234 stop
# 3. View a summary of what was captured
java -jar condensed-data.jar summary recording.cjfr
# 4. Open an event view (e.g. GC pauses, hot methods)
java -jar condensed-data.jar view recording.cjfr gc-pauses
To view interactively with SQL queries, open the file in jfr-query or drag-and-drop onto the hosted web UI.
GC pauses are growing — capture a long recording to see the trend
GC problems often build up over hours. Start a recording, let it run while the JVM degrades, then stop and look at the pause trend:
# Attach and start (runs until you stop it)
java -jar condensed-data.jar agent <PID> start gc-investigation.cjfr
# After the issue reproduces (minutes or hours later):
java -jar condensed-data.jar agent <PID> stop
# View GC pause trend
java -jar condensed-data.jar view gc-investigation.cjfr gc-pauses
Open in jfr-query for a timeline chart
showing how pause duration evolves over the recording period.
Look for: increasing pause frequency, pause cause changing from G1YoungGeneration
to G1OldGeneration, or heap-after-GC growing steadily (heap leak).
Keep a rolling window of diagnostics without filling disk
In production you rarely know in advance when a problem will occur. Use rotating files to keep the last N MB of history always available:
java -jar condensed-data.jar agent <PID> start \
--rotating --max-size=100m --max-files=5 \
recording_$index.cjfr
When an incident occurs, stop the agent — the most recent files covering the last ~500 MB of events are on disk. Condense them for analysis:
java -jar condensed-data.jar agent <PID> stop
java -jar condensed-data.jar view recording_0.cjfr gc-pauses
<PID> can also be a main-class name filter or all to target all matching JVMs.
Record with rotating files (production use)
Keep at most 3 files of 100 MB each — old files are overwritten:
java -jar condensed-data.jar agent <PID> start \
--rotating --max-size=100m --max-files=3 \
recording_$index.cjfr
Or start via -javaagent at JVM launch (comma-separated, no dashes):
java -javaagent:condensed-data.jar=start,rotating,max-size=100m,max-files=3,recording.cjfr \
-jar myapp.jar
Condense existing JFR files
# Single file:
java -jar condensed-data.jar condense myrecording.jfr output.cjfr
# Whole folder or ZIP:
java -jar condensed-data.jar condense recordings/ output.cjfr
# Inflate back to JFR for tools that don't support .cjfr:
java -jar condensed-data.jar inflate output.cjfr restored.jfr
View events and summaries
# Print a summary:
java -jar condensed-data.jar summary recording.cjfr
# Use JFR named views (gc-pauses, hot-methods, allocation-by-site, …):
java -jar condensed-data.jar view recording.cjfr gc-pauses
# Open in jfr-query notebook UI:
java -jar query.jar serve recording.cjfr
# or drag-and-drop onto https://parttimenerd.github.io/jfr-query/
Open in JDK Mission Control
A JMC fork supports native .cjfr files —
no inflation step required:
# Download snapshot build from:
# https://github.com/parttimenerd/jmc/releases/tag/snapshot
# Then open JMC and File > Open > select your .cjfr file
Or inflate first if you need the standard JMC release:
java -jar condensed-data.jar inflate recording.cjfr recording.jfr
No stable release yet — rolling snapshots only. Check docs/jar-releases.md for which JAR variant to download for your environment.
Reference site listing every JFR event across JDK versions, with field names, types, descriptions, and benchmark-derived examples showing which events fire at what rate. Use it when you are writing JFR tooling and need to know exactly which fields an event has or which events are worth subscribing to.
When to use
- You need to know what fields a JFR event has across JDK versions
- You are writing JFR tooling and need event documentation
- You want to understand which events are worth subscribing to for a given use case
- You need to compare event availability between JDK 17, 21, and 24
When not to use
- You need a programmatic API to enumerate events at runtime (use JFR API directly)
- You need offline access (web-only reference site)
Canonical site is at sap.github.io/jfrevents/. Maintained by the SAP SapMachine team.
Educational sampling CPU profiler for Java written in pure Java 17. Uses Thread.getAllStackTraces() to sample stacks and outputs a method table and an HTML flame graph. Safepoint-biased by design — intended to demystify how profilers work, not for production use.
When to use
- You want to understand how a sampling profiler works from first principles
- You need a quick profiler with no native dependencies on any platform
When not to use
- You need accurate profiling — safepoint-biased, misses async and JIT-compiled hot paths
- You need production profiling — use async-profiler or JFR instead
Install
No stable release yet — check GitHub for build instructions.
Usage
java -javaagent:target/tiny-profiler.jar=flamegraph=flame.html YourApp
# interval=10 (ms) is the default; override with interval=5
java -javaagent:target/tiny-profiler.jar=flamegraph=flame.html,interval=5 YourApp
Find more information at https://github.com/parttimenerd/tiny-profiler
Safepoint-biased — only samples at JVM safepoints, so CPU-heavy native or JIT code may be underrepresented.
Fast heap dump analyser (Rust CLI). Reproduces Eclipse MAT's System Overview, Leak Suspects, Top Consumers, and Threads views with a fraction of the memory overhead. Handles dumps larger than 10 GiB that MAT cannot open. Ships an OQL query engine, a disk-cached interactive analysis mode, an MCP server for AI-assisted triage, an HTTP server for the browser UI and LLM agent access, a redact subcommand to zero sensitive data before sharing, MAT cache generation, and an in-browser WASM UI for dumps up to 3 GiB.
When to use
- You need to analyse a heap dump without Eclipse MAT's memory overhead
- You have a very large heap dump (>10 GiB) that MAT cannot handle
- You want a quick in-browser analyser for smaller dumps (WASM, no install)
- You want Leak Suspects, Top Consumers, and thread views without a GUI IDE
- You need scriptable, CI-friendly heap analysis (JSON output, diff two dumps)
- You want AI-assisted heap triage via Claude, Cline, or other MCP clients
- You want to query a heap dump with OQL from the CLI or an interactive REPL
- You need to redact sensitive data from a dump before sharing it
- You want to speed up Eclipse MAT's first open of a large dump (cache generation)
When not to use
- You need full interactive MAT-style object graph exploration (use Eclipse MAT)
Install
brew tap parttimenerd/hprof-analyzer
brew trust parttimenerd/hprof-analyzer # required once for third-party taps
brew install hprof-analyzer
Usage
hprof-analyzer heap.hprof report.html
Find more information at https://parttimenerd.github.io/hprof-analyzer/
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Analyse a heap dump and open the report
hprof-analyzer heap.hprof report.html
Opens a self-contained HTML file with System Overview, Leak Suspects, Top Consumers, and Threads — no server, no external assets.
Compressed dumps are read transparently:
hprof-analyzer heap.hprof.gz report.html
hprof-analyzer heap.hprof.zip report.html
Tip: use -XX:+HeapDumpGzip to write compressed dumps directly — typically 5–10× smaller.
Update to the latest nightly build at any time:
hprof-analyzer update nightly
Find what is consuming the most memory
The Top Consumers view ranks object types by retained heap — the amount of memory that would be freed if all instances of that class were collected:
hprof-analyzer heap.hprof report.html
# Open report.html → "Top Consumers" tab
For a fast command-line answer without a full report:
hprof-analyzer heap summary heap.hprof
Look for classes with high retained bytes — those are the real memory holders, not just large arrays that are themselves held by something else.
Find the likely cause of an OutOfMemoryError
The Leak Suspects view groups objects into accumulation points — classes where many instances exist that share a common path from a GC root:
hprof-analyzer heap.hprof report.html
# Open report.html → "Leak Suspects" tab
Each suspect shows the accumulator class, instance count, total retained bytes, and the shortest path from a GC root to a representative instance. For a text summary from the cache (fast after first run):
hprof-analyzer heap summary heap.hprof
If the suspect is a framework collection (e.g. a HashMap inside a cache),
check which application class holds a reference to it — that is where to look
for a missing eviction policy or unbounded growth.
Analyse in the browser without installing anything
.hprof file. Runs entirely via WebAssembly — no data leaves your machine.
Performance depends on available browser memory; it may work fine above 3 GiB or struggle below
it depending on your machine. For large or slow dumps use the native binary instead.Repeatedly explore the same dump with fast cached access
The heap subcommand group runs the full analysis once, writes a cache alongside the dump,
and loads in ~1 s on every subsequent call:
# First run: full analysis (5–15 min on large dumps)
hprof-analyzer heap summary heap.hprof
# All subsequent runs: ~1 s from cache
hprof-analyzer heap summary heap.hprof
hprof-analyzer heap histogram heap.hprof
hprof-analyzer heap query heap.hprof --oql "SELECT COUNT(*) FROM java.lang.String"
hprof-analyzer heap browse heap.hprof # browse dominator tree
hprof-analyzer heap inspect heap.hprof --index 42
The cache lives at <dump>.hprof-cache/ alongside the file. Delete it to force re-analysis.
Run OQL queries — CLI REPL or browser
Interactive REPL in the terminal with tab-completion:
hprof-analyzer query heap.hprof --repl
Or start a local server and use the browser OQL shell:
hprof-analyzer server heap.hprof
# → open http://127.0.0.1:7070
/help oql — full OQL language reference
/examples — guided tour of OQL examples by category
The OQL dialect is modelled on Eclipse MAT’s — most MAT queries work unchanged.
Use with Claude or Cline via MCP
hprof-analyzer ships a built-in MCP server so Claude, Cline, and other MCP-compatible AI assistants can analyse heap dumps directly.
Claude Code:
claude mcp add hprof -- hprof-analyzer mcp
Cline (VS Code) — add to .vscode/mcp.json:
{ "mcpServers": { "hprof": { "command": "hprof-analyzer", "args": ["mcp"] } } }
Homebrew prints full MCP setup instructions after install.
Redact a dump before sharing
Heap dumps contain sensitive data — string values, field contents, serialized objects.
The redact subcommand zeroes primitive values while preserving class/field names and
the object graph, producing a dump safe to share with colleagues or attach to a bug report:
# Lean mode (default): zeroes all primitive array elements (byte[], char[], int[], …)
hprof-analyzer redact heap.hprof redacted.hprof
# Complete mode: also zeroes scalar instance fields and static fields
hprof-analyzer redact --complete heap.hprof redacted.hprof
Redacted dumps are readable by hprof-analyzer, Eclipse MAT, and jhat.
hprof-analyzer detects them automatically and shows a “Redacted dump” banner,
skipping analyses that would be meaningless on zeroed data.
A standalone hprof-redact binary (under 1 MB) is also available for minimal environments.
Generate Eclipse MAT cache files (speed up first MAT open)
For large dumps MAT’s first parse can peak at ~55 GiB RSS. hprof-analyzer generates the same cache files with much lower memory usage:
hprof-analyzer mat caches heap.hprof
Open heap.hprof in MAT as usual — it detects the cache and skips the expensive first parse.
The in-browser WASM version is memory-limited by the browser — it may work for larger dumps or struggle for smaller ones depending on your machine. For reliable analysis of large dumps use the native binary.
Deprecated standalone redaction tool — the `hprof-analyzer redact` subcommand now supersedes this for CLI use. The underlying stream-based library (zeroes string contents and primitive values without loading the full file into memory) remains available for programmatic pipeline integration, e.g. CF plugin tooling.
When to use
- You are building tooling that needs to redact heap dumps programmatically (library use)
When not to use
- You want to redact a dump interactively — use hprof-analyzer's built-in redact subcommand instead
- You need guaranteed complete privacy — not security-audited
Related
Install
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>hprof-redact</artifactId>
<version>0.3.0</version>
</dependency>
Usage
hprof-analyzer redact input.hprof redacted.hprof
Find more information at https://github.com/parttimenerd/hprof-redact
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Redact a heap dump before sharing
For standalone use, call hprof-analyzer redact — it uses this library internally
and is the recommended way to redact without writing code:
hprof-analyzer redact heap.hprof redacted.hprof
The redacted dump zeroes all string values and primitive field contents while keeping class/field/method names intact. hprof-analyzer detects it automatically and shows a “Redacted dump” banner in the report.
Redact programmatically in a pipeline
Add the Maven dependency and call HprofRedactor.redact() in a stream pipeline —
the library never loads the full dump into memory:
import me.bechberger.HprofRedactor;
import java.nio.file.Path;
HprofRedactor.redact(
Path.of("heap.hprof"),
Path.of("heap-redacted.hprof")
);
Use this when building CF plugin tooling or any pipeline that needs to strip sensitive data before uploading a dump to a remote system.
Deprecated for standalone use — `hprof-analyzer redact` supersedes this. Library is still usable for programmatic pipeline integration. Not security-audited — review before using for compliance purposes.
Java library and CLI for parsing, transforming, and redacting HotSpot hs_err crash report files. Full-fidelity round-trip parsing into a typed model with 20+ section types, visitor and transformer patterns for selective modification, and configurable redaction of sensitive data before sharing.
When to use
- You need to parse hs_err files programmatically and access threads, stack frames, registers, or VM info
- You want to redact sensitive data (usernames, paths, PIDs, env vars) before sharing crash reports
- You need to convert hs_err files to JSON for tooling or storage
When not to use
- You just need to read a crash report — open it in a text editor or use the VS Code extension
Related
Install
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>jhserr</artifactId>
<version>0.1.0</version>
</dependency>
Usage
HsErrReport report = HsErrParser.parse(Path.of("hs_err_pid12345.log"));
System.out.println(report.header().errorType() + ": " + report.header().errorDetail());
HsErrReport redacted = RedactionTransformer.withDefaults().transform(report);
Files.writeString(Path.of("redacted.log"), redacted.toString());
Find more information at https://parttimenerd.github.io/jhserr/
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Parse a crash report and inspect what crashed
Point HsErrParser.parse() at the file — error type, signal, problematic frame,
and JVM version are in the header:
import me.bechberger.jhserr.HsErrParser;
import me.bechberger.jhserr.model.HsErrReport;
HsErrReport report = HsErrParser.parse(Path.of("hs_err_pid12345.log"));
System.out.println(report.header().errorType()); // e.g. SIGSEGV
System.out.println(report.header().errorDetail()); // problematic frame
System.out.println(report.vmInfo().version()); // JVM version string
// Java frames from the crashing thread:
report.currentThread().javaFrames()
.forEach(f -> System.out.println(f.method()));
Or get a quick summary without writing code:
java -jar jhserr.jar summary hs_err_pid12345.log
Redact a crash report before sharing
The default preset strips usernames, paths, hostnames, PIDs, IP addresses, and env vars.
Use --minimal or --aggressive for lighter or heavier redaction:
java -jar jhserr.jar redact hs_err_pid12345.log -o redacted.log
java -jar jhserr.jar redact --aggressive hs_err_pid12345.log -o redacted.log
java -jar jhserr.jar redact --scan hs_err_pid12345.log # scan only, no output
Add extra hints for paths or usernames not auto-detected:
java -jar jhserr.jar redact --username jdoe --sensitive-path /home/jdoe/project \
hs_err_pid12345.log -o redacted.log
Walk the parsed model with the visitor pattern
Override only the visit methods you need — all default to no-ops:
report.accept(new HsErrVisitor() {
@Override
public void visitJavaFrames(FrameList frames) {
frames.frames().forEach(f -> System.out.println(f.method()));
}
@Override
public void visitEnvironmentVariables(EnvironmentVariables vars) {
vars.variables().forEach((k, v) -> System.out.println(k + "=" + v));
}
});
Convert to JSON for tooling
String json = HsErrJson.toJson(report);
HsErrReport restored = HsErrJson.fromJson(json); // round-trip
Or via CLI:
java -jar jhserr.jar json to hs_err.log > report.json
java -jar jhserr.jar json from report.json > restored.log
java -jar jhserr.jar json schema > schema.json
Early prototype — no guarantees regarding functionality or security.
Interactive browser tool for exploring OpenJDK's -Xlog subsystem. Enter a log config and see exactly which log statements match — with source context, GitHub permalinks, sample output from benchmark runs, and a volume estimate. Use it when you are tuning GC, JIT, or other -Xlog flags and want to understand what output you will get before deploying.
Features
- Config search with autocomplete over all -Xlog tags and levels
- Selector wizard — toggle tags on/off without knowing the syntax
- Firing log sites grouped by file with surrounding source context and GitHub permalinks
- Real sample output captured from benchmark runs (G1, ZGC, Parallel)
- Volume estimate (≈ MB/hour) per tag to gauge logging overhead
- Coverage across JDK versions from LTS to master
Data is regenerated monthly from the OpenJDK source and redeployed to GitHub Pages.
Maven and Gradle plugin that reorders your test suite so tests covering recently changed code run first. Uses bytecode instrumentation to track class-to-test coverage, then on the next run promotes affected tests to the front — zero config, no cloud, no annotations.
When to use
- You want faster feedback in CI by surfacing relevant failures early
- Your test suite is large enough that waiting for the full run hurts productivity
- You want zero-config test prioritisation with Maven or Gradle
When not to use
- You need cross-module coverage tracking (single-module only for now)
- You are not using Maven or Gradle
Install
<plugin>
<groupId>me.bechberger</groupId>
<artifactId>test-order-maven-plugin</artifactId>
<version>0.1.1</version>
<extensions>true</extensions>
<executions>
<execution>
<goals><goal>prepare</goal></goals>
</execution>
</executions>
</plugin>
Usage
mvn test # first run: learns coverage
mvn test # second run: affected tests go first
Find more information at https://parttimenerd.github.io/test-order/
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Verify reordering is working after the first run
After the first mvn test (which collects coverage data), run it again and check
that affected tests moved to the front:
mvn test # first run — collects coverage, no reordering yet
# edit a source file, then:
mvn test # second run — tests covering changed code run first
Confirm the ranking with:
mvn test-order:show
# or the fully-qualified form if you haven't set up the prefix:
mvn me.bechberger:test-order-maven-plugin:show
If the list is empty or tests are not reordered, run the diagnostics command to find out why — see “Diagnose why reordering isn’t working” below.
Enable the short mvn test-order: prefix
Maven only resolves goal prefixes for trusted groupIds. Add me.bechberger to
~/.m2/settings.xml once so the short form works from the CLI:
<settings>
<pluginGroups>
<pluginGroup>me.bechberger</pluginGroup>
</pluginGroups>
</settings>
Without this, use the fully-qualified form:
mvn me.bechberger:test-order-maven-plugin:show
Inspect the test ranking and run affected tests only
Maven:
mvn test-order:show # ranked list of all tests
mvn test-order:affected test # run only tests affected by recent changes
mvn test-order:dashboard # full HTML report
Gradle:
./gradlew testOrderShow
./gradlew testOrderAffected
./gradlew testOrderDashboard
Detect flaky and order-dependent tests
mvn test-order:detect-dependencies # Maven
./gradlew testOrderDetectDependencies # Gradle
Runs the suite in multiple orders to surface tests that only pass in a specific sequence.
Diagnose why reordering isn't working
mvn test-order:diagnose # Maven
./gradlew testOrderDiagnose # Gradle
Checks prerequisites (Java 17+, Maven 3.6+/Gradle 7.6+, Git), coverage data presence, and plugin configuration. Run this first if tests are not being reordered.
Maven plugin and CLI that turns a fat JAR into a single self-executing file on Linux and macOS. Prepends a small POSIX sh launcher that finds Java automatically, checks min/max version requirements, and execs the JAR — no java -jar needed.
When to use
- You want to ship a CLI tool as a single executable without requiring users to run java -jar
- You need automatic Java discovery and min/max version checking at startup
When not to use
- You are on Windows (Linux/macOS only)
- You are packaging a library JAR (requires a fat/uber JAR with all dependencies)
Install
<plugin>
<groupId>me.bechberger</groupId>
<artifactId>execjar</artifactId>
<version>0.1.2</version>
<executions>
<execution>
<goals><goal>execjar</goal></goals>
</execution>
</executions>
</plugin>
Usage
mvn package
./target/your-app # no java -jar needed
Find more information at https://github.com/parttimenerd/execjar
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Set up the two-step Maven build
execjar must run after a fat JAR has been assembled. Add it after your assembly or shade plugin:
<!-- Step 1: assemble fat JAR -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs>
<archive><manifest><mainClass>com.example.App</mainClass></manifest></archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals><goal>single</goal></goals>
</execution>
</executions>
</plugin>
<!-- Step 2: wrap it -->
<plugin>
<groupId>me.bechberger</groupId>
<artifactId>execjar</artifactId>
<version>VERSION</version>
<executions>
<execution>
<goals><goal>execjar</goal></goals>
</execution>
</executions>
</plugin>
After mvn package, run the output directly — the launcher searches $JAVA_HOME, then PATH,
then /etc/alternatives/java, then common install locations under /Library/Java and /usr/lib/jvm:
./target/your-app
Enforce a minimum (or maximum) Java version
Add <minJavaVersion> and/or <maxJavaVersion> to the plugin configuration:
<configuration>
<minJavaVersion>17</minJavaVersion>
<maxJavaVersion>21</maxJavaVersion>
</configuration>
minJavaVersion defaults to maven.compiler.release or maven.compiler.target if set, so you
often don’t need to configure it explicitly. The launcher prints a clear error and exits if the
requirement isn’t met.
Embed JVM options and system properties
JVM flags and system properties can be baked into the launcher so users don’t need to set them:
<configuration>
<jvmOpts>-Xmx512m -XX:+UseG1GC</jvmOpts>
<javaProperties>
<app.name>My Application</app.name>
<app.config>/opt/myapp/config</app.config>
</javaProperties>
</configuration>
Environment variables, prepended/appended default arguments, and strict-mode (exact version match) are also supported — see the README for the full configuration reference.
Debug Java discovery at runtime
Set EXECJAR_DEBUG=1 before running to print the selected Java executable, detected version,
resolved JVM options, and the final exec command:
EXECJAR_DEBUG=1 ./target/your-app
This is useful when the wrong Java is picked up or version checks fail unexpectedly.
Java agent that gives every lambda expression a human-readable toString() at zero per-invocation cost — label computed once at class-load time, injected as a Proxy.
When to use
- You need to log or debug lambda expressions stored in queues, fields, or collections and the default JVM toString() (e.g. `$$Lambda/0x...@1b6d3586`) is unreadable
- You want clean stack traces that show which lambda was executing and where it was created, without modifying source code
- You are debugging executor pools, callback registries, Spring event listeners, or other lambda-heavy frameworks
When not to use
- Your lambdas implement `Serializable` — they are intentionally left unwrapped to preserve serialization round-trips
- You are targeting JDK-internal functional chains (`andThen`, `compose`, `negate`) created by JDK classes loaded before agent installation
- Your application is extremely latency-sensitive at the lambda invocation level (each Proxy dispatch adds ~35 ns; negligible for real work, visible in tight microbenchmarks)
Install
java -javaagent:lambda-stringer.jar -jar your-app.jar
Usage
Runnable task = () -> processOrder(orderId);
log.warn("stuck task: {}", task);
// → stuck task: Lambda[Runnable @ OrderService.processOrder(OrderService.java:88)]
Find more information at https://github.com/parttimenerd/lambda-stringer
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Attach the agent at JVM startup
Download the jar and pass it as -javaagent:
curl -L -o lambda-stringer.jar \
https://github.com/parttimenerd/lambda-stringer/releases/download/latest/lambda-stringer.jar
java -javaagent:lambda-stringer.jar -jar your-app.jar
No source changes required. Every lambda’s toString() now returns a human-readable label.
Use with Maven Surefire (test-scoped)
Add the dependency and configure Surefire to attach it during tests:
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>lambda-stringer</artifactId>
<version>VERSION</version>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/me/bechberger/lambda-stringer/VERSION/lambda-stringer-VERSION.jar</argLine>
</configuration>
</plugin>
Customize the label format
Pass a format= argument to the agent, or set a system property before agent loads.
Available tokens:
| Token | Meaning | Example |
|---|---|---|
%i | functional interface simple name | Runnable |
%I | functional interface fully-qualified name | java.lang.Runnable |
%s | enclosing class simple name | Foo |
%S | enclosing class fully-qualified name | com.example.Foo |
%c | alias for %S | com.example.Foo |
%m | enclosing method name | bar |
%f | source file name | Foo.java |
%l | line number (? if unavailable) | 42 |
%% | literal % | % |
# Compact format for log parsers
java -javaagent:lambda-stringer.jar=format=%S#%m:%l -jar your-app.jar
# → com.example.Scheduler#buildTask:42
# Simple class name only
java -Dlambda.tostring.format="%i@%s:%l" -javaagent:lambda-stringer.jar ...
# → Runnable@Scheduler:42
Read clean stack traces
Agent frames (LambdaHandler, $Proxy, invokeWithArguments) are stripped automatically.
A // ^ via Lambda[…] annotation is injected after the executor frame so you can see both
who called the lambda and where it was defined:
java.lang.RuntimeException: task failed
at com.example.OrderService.lambda$process$2(OrderService.java:91)
at com.example.Registry.runAll(Registry.java:12)
at // ^ via Lambda[Runnable @ OrderService.setup(OrderService.java:88)]
at com.example.App.main(App.java:20)
Attach dynamically without restarting the JVM
Use agentmain via the Attach API:
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("/path/to/lambda-stringer.jar");
vm.detach();
Lambdas in classes loaded after attach will get labels. Already-loaded classes are not retroactively instrumented.
Java agent that instruments other Java agents — wraps every ClassFileTransformer it finds and records bytecode diffs viewable via a built-in web UI. Shows exactly what Mockito, Dynatrace, async-profiler, or any other agent does to your classes at runtime.
When to use
- You want to see exactly what bytecode transformations a Java agent (Mockito, Dynatrace, async-profiler) applies
- You are debugging unexpected behaviour caused by another agent's class instrumentation
- You want to intercept or block specific ClassFileTransformer registrations at runtime
When not to use
- You only want to view class bytecode without running another agent — use a decompiler instead
Install
git clone https://github.com/parttimenerd/meta-agent
cd meta-agent
mvn package -DskipTests
# JAR: target/meta-agent.jar
Usage
java -javaagent:target/meta-agent.jar=server -jar your-program.jar
# open http://localhost:7071 to browse transformations
Find more information at https://github.com/parttimenerd/meta-agent
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.View what an agent does to your classes
Attach meta-agent alongside the agent you want to inspect. Open the built-in web UI to browse decompiled bytecode diffs grouped by transformer or by class:
java -javaagent:target/meta-agent.jar=server -jar your-program.jar
# custom port:
java -javaagent:target/meta-agent.jar=server,port=8080 -jar your-program.jar
Key endpoints at http://localhost:7071:
/instrumentators— list all ClassFileTransformers that were registered/full-diff/instrumentator?pattern=.*— decompiled bytecode diff per transformer/classes— list all classes that were transformed/full-diff/class?pattern=.*— full diff across all classes and transformers/all/decompile?pattern=<pattern>— decompile matching classes
Intercept or block transformer registrations with a callback
Implement InstrumentationCallback and pass it via the cb argument to observe or block
transformers. Note: CallbackAction.IGNORE suppresses the transformer (not DENY):
public class MyHandler implements InstrumentationCallback {
@Override
public CallbackAction onAddTransformer(ClassFileTransformer transformer) {
System.err.println("New transformer: " + transformer.getClass().getName());
return CallbackAction.ALLOW; // or IGNORE to suppress it
}
@Override
public void onExistingTransformer(ClassFileTransformer transformer) {
System.err.println("Existing transformer: " + transformer.getClass().getName());
}
@Override
public CallbackAction onInstrumentation(ClassFileTransformer transformer,
ClassArtifact before, ClassArtifact after) {
System.out.println("Instrumenting: " + before.klass().getName());
return CallbackAction.ALLOW;
}
}
Pass the fully qualified class name via cb:
java -javaagent:target/meta-agent.jar=server,cb=com.example.MyHandler -jar your-program.jar
Capture native agents too
Build the native component first, then load both agents:
cd native && make
# macOS:
java -agentpath:native/native_agent.dylib \
-javaagent:target/meta-agent.jar=server \
-jar your-program.jar
# Linux:
java -agentpath:native/native_agent.so \
-javaagent:target/meta-agent.jar=server \
-jar your-program.jar
Add to Maven test runs
Use the Maven plugin to attach meta-agent automatically during mvn test:
<plugin>
<groupId>me.bechberger</groupId>
<artifactId>meta-agent-maven-plugin</artifactId>
<version>VERSION</version>
<executions>
<execution>
<phase>validate</phase>
<goals><goal>meta-agent</goal></goals>
</execution>
</executions>
<configuration>
<server>true</server>
<callbackClasses>
<callbackClass>com.example.MyHandler</callbackClass>
</callbackClasses>
</configuration>
</plugin>
Add the runtime dependency so your callback class can reference the API:
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>meta-agent</artifactId>
<version>VERSION</version>
<scope>test</scope>
</dependency>
Java library for parsing thread dump output from jstack and jcmd into a typed model. Handles all major thread dump formats across JDK versions, including virtual threads, giving you structured access to thread states, stack frames, lock information, CPU times, and deadlock cycles.
When to use
- You need to programmatically parse and analyse Java thread dumps
- You are building tooling that processes jstack or jcmd output
- You want to diff thread dumps or detect recurring blocking patterns
- You need virtual thread support in thread dump parsing
When not to use
- You need live thread inspection (use jstall instead)
- You need profiling data (use the JFR profiling tools instead)
Install
<dependency>
<groupId>me.bechberger</groupId>
<artifactId>jthreaddump</artifactId>
<version>0.5.8</version>
</dependency>
Usage
ThreadDump dump = ThreadDumpParser.parse(dumpText);
for (var thread : dump.threads()) {
System.out.println(thread.name() + " - " + thread.state());
}
if (dump.deadlockInfos() != null && !dump.deadlockInfos().isEmpty()) {
System.out.println("Deadlocks detected!");
}
Find more information at https://github.com/parttimenerd/jthreaddump
How To
These examples may be incomplete or outdated — see the README / docs for the full reference.Parse a thread dump file and inspect threads
Feed any jstack or jcmd Thread.print output to ThreadDumpParser.parse() —
you get a typed model with states, stack frames, and deadlock detection:
import me.bechberger.jthreaddump.ThreadDumpParser;
import me.bechberger.jthreaddump.model.ThreadDump;
String raw = Files.readString(Path.of("dump.txt"));
ThreadDump dump = ThreadDumpParser.parse(raw);
System.out.println("Threads: " + dump.threads().size());
dump.threads().forEach(t ->
System.out.println(t.name() + " — " + t.state()));
if (!dump.deadlockInfos().isEmpty()) {
System.out.println("Deadlock detected!");
}
Works with output from jstack <pid>, jcmd <pid> Thread.print, and thread dumps
in .txt files captured by any means.
Capture a live thread dump and parse it
String raw = JStackUtil.captureThreadDump(pid); // uses jstack
String raw = JStackUtil.captureThreadDump(pid, true); // uses jcmd (Thread.print -l)
ThreadDump dump = ThreadDumpParser.parse(raw);
jcmd output includes more detail: CPU time, elapsed time, and JNI reference counts.
Find BLOCKED threads and the locks they are waiting for
ThreadDump dump = ThreadDumpParser.parse(dumpText);
dump.threads().stream()
.filter(t -> t.state() == Thread.State.BLOCKED)
.forEach(t -> System.out.println(t.name() + " is blocked"));
Lock information (monitors held/waiting, object addresses) is available on each thread object.
Find threads consuming the most CPU time
CPU time is populated when using jcmd output (JStackUtil.captureThreadDump(pid, true)).
dump.threads().stream()
.filter(t -> t.cpuTimeSec() != null)
.sorted((a, b) -> Double.compare(b.cpuTimeSec(), a.cpuTimeSec()))
.limit(10)
.forEach(t -> System.out.printf("%s: %.2f sec CPU%n", t.name(), t.cpuTimeSec()));
Diff two thread dumps
ThreadDump before = ThreadDumpParser.parse(Files.readString(Path.of("before.txt")));
ThreadDump after = ThreadDumpParser.parse(Files.readString(Path.of("after.txt")));
System.out.println("Thread delta: " + (after.threads().size() - before.threads().size()));
Use this pattern to compare dumps taken during a suspected leak or thread explosion.
Use the CLI to pretty-print a dump
The library ships a minimal CLI for quick inspection:
jthreaddump dump.txt # parse and display summary
jstack <pid> | jthreaddump - # pipe from jstack
jthreaddump dump.txt -v # verbose output
Build it with mvn clean package, then run java -jar target/jthreaddump.jar.
Companion code for the "Writing a Profiler from Scratch" blog series. A minimal C++/JVMTI sampling profiler that uses AsyncGetCallTrace via signal-based interrupts — showing the core mechanics of how production profilers like async-profiler work under the hood.
When to use
- You are reading the blog series and want to run the example code
- You want to study the core mechanics of a signal-based JVMTI sampling profiler
When not to use
- You need a production profiler — use async-profiler or JFR instead
- You want a pure-Java profiler — see tiny-profiler instead
Install
No stable release yet — check GitHub for build instructions.
Usage
# macOS:
java -agentpath:cpp/libSmallProfiler.dylib=interval=0.001s -cp samples BasicSample
# Linux:
java -agentpath:cpp/libSmallProfiler.so=interval=0.001s -cp samples BasicSample
Find more information at https://github.com/parttimenerd/writing-a-profiler
Don't set the interval too low or you will crash the JVM. Educational code only — not for production use.