Profiling another project with jvmlens
A task-oriented guide for adopting jvmlens in any other JVM project. The worked example is a project called builder — substitute your own jar, pid, and package prefix. "Profile builder from builder" just means running the jvmlens jar from inside the builder checkout against the builder process.
| This mirrors the repository’s INTEGRATING.md, which is the portable copy-paste version you can hand to another repo. For the full flag reference see Usage. |
What you need first
jvmlens is not yet on Maven Central, but every green build on main publishes a
rolling latest pre-release, so you can grab the jars without building. Java 17+.
mkdir -p tools
curl -L -o tools/jvmlens.jar https://github.com/alexmond/jvmlens/releases/download/latest/jvmlens.jar
curl -L -o tools/jvmlens-agent.jar https://github.com/alexmond/jvmlens/releases/download/latest/jvmlens-agent.jar
curl -L -o tools/jvmlens-jmh.jar https://github.com/alexmond/jvmlens/releases/download/latest/jvmlens-jmh.jar
The URLs are stable (always the most recent green build); it’s a pre-release, so APIs may change until the first tagged release. Or build from source:
git clone https://github.com/alexmond/jvmlens && cd jvmlens
./mvnw -q clean package
# jvmlens-cli/target/jvmlens.jar <- CLI + MCP server (analyze/profile/watch/mcp)
# jvmlens-agent/target/jvmlens-agent.jar <- the in-process -javaagent jar
Nothing about jvmlens needs to be a dependency of your build — it consumes JFR, which the JDK already produces. jvmlens never calls an LLM and never ships your recording anywhere; every path below runs locally.
Pick your path
| You have… | You want… | Path |
|---|---|---|
A |
A one-shot summary |
A — |
A class with a |
A repeatable warm-loop benchmark |
A2 — |
A running JVM (local or one |
An on-demand snapshot |
B — |
A long-running service / CI job / container |
Continuous summaries, zero attach |
C — |
A coding agent that should fetch profiles itself |
Profiling exposed as tools |
D — |
A k8s workload |
A sidecar that doesn’t touch your app’s chart |
E — Helm |
Paths compose — run the agent © in k8s (E), or point an MCP client (D) at a remote
host over ssh.
Path A — analyze a recording
java -jar tools/jvmlens.jar analyze builder-run.jfr # markdown (default)
java -jar tools/jvmlens.jar analyze -r cpu builder-run.jfr # focus: cpu|memory|locks|gc
java -jar tools/jvmlens.jar analyze -f prompt builder-run.jfr # wrapped as an LLM task
Capture a recording around a builder workload:
java -XX:StartFlightRecording=duration=30s,filename=builder-run.jfr,settings=profile \
-jar build/libs/builder.jar build ./big-project
Optimize → measure loop (diff, gate, JMH)
java -jar tools/jvmlens.jar analyze /tmp/run-after -a com.example.builder # merge JMH -prof jfr forks
java -jar tools/jvmlens.jar analyze --baseline /tmp/run-before /tmp/run-after # diff (absolute-anchored)
java -jar tools/jvmlens.jar analyze -b before.jfr after.jfr --assert "alloc-pct < 0, gc-pct < 10" # CI gate
java -jar tools/jvmlens.jar analyze builder-run.jfr --hints --top-k 3 # fix directions + budget size
# inline from a JMH benchmark (put jvmlens-jmh.jar on the classpath):
java -cp benchmarks.jar:tools/jvmlens-jmh.jar org.openjdk.jmh.Main \
-prof "org.alexmond.jvmlens.jmh.JvmlensProfiler:appPackage=com.example.builder"
Path A2 — bench a main (no JMH module)
Most apps don’t have a JMH module. bench is the no-JMH harness: point it at any
main(String[]) and it runs a warmup→timed loop, captures a JFR over only the timed phase,
and summarizes — no hand-rolled driver, no pre-recorded file:
java -jar tools/jvmlens.jar bench --main com.example.builder.BuildDriver \
--cp build/libs/builder.jar -w 20 -i 200 -a com.example.builder --jfr /tmp/before.jfr
--cp loads the workload (it needn’t be on jvmlens’s classpath); --jfr keeps the recording
so it can be a --baseline for the after-run. Write a tiny driver main that exercises the
hot path once; bench is the loop.
Path B — profile a running process
profile <pid> attaches to a live JVM, records a timed window, and summarizes it — no
pre-recorded file, no JMX, no start-up flags on the target.
PID=$(pgrep -f 'builder.jar' | head -1)
java -jar tools/jvmlens.jar profile "$PID" # 20s, markdown
java -jar tools/jvmlens.jar profile -d 30 -w 5 "$PID" # skip 5s startup, record 30s
java -jar tools/jvmlens.jar profile -e async -d 30 "$PID" # native frames (local pid only)
Remote builder — run jvmlens on the host; only the few-hundred-token summary travels back:
ssh build-host 'java -jar jvmlens.jar profile $(pgrep -f builder.jar) -f prompt'
kubectl exec builder-pod -- java -jar /tools/jvmlens.jar profile 1 -r cpu
Use -w/--warmup to measure the steady-state build, not classloading. A ⚠
adequacy caveat means too few samples — record longer or under load.
Path C — always-on, in-process (agent)
The agent keeps a JFR ring buffer inside builder and writes a fresh summary every interval — no attach.
java -javaagent:tools/jvmlens-agent.jar=out=/var/log/builder-profile.md,interval=60 \
-jar build/libs/builder.jar
| Key | Meaning | Default |
|---|---|---|
|
File the latest summary is written to (overwritten each interval) |
|
|
JSONL file the agent appends one sample to per interval (multi-day trends) |
— |
|
Seconds between summaries |
|
|
JFR config: |
|
|
|
— |
For a multi-day watch, add history=<file.jsonl> (the agent appends one CPU+memory+wait
sample per interval) and reduce the run later with jvmlens trend <file.jsonl> — a
change-over-time digest with a hedged retention indicator, never a confident "leak".
In containers, set it via the JVM-standard env var (honoured by buildpacks and
java -jar alike):
JAVA_TOOL_OPTIONS=-javaagent:/agent/jvmlens-agent.jar=out=/agent/builder.md,interval=60
Path D — MCP for coding agents
jvmlens mcp runs a Model Context Protocol server over stdio, exposing scoped,
navigable tools (overview → hot_paths / hot_leaves / allocations /
lock_contention) plus a live profile tool. It serves data only — never calls an
LLM, so recordings stay on the host.
{ "mcpServers": {
"jvmlens": { "command": "java", "args": ["-jar", "/abs/path/builder/tools/jvmlens.jar", "mcp"] }
} }
Remote builder, no extra ports — the client launches jvmlens over ssh:
{ "mcpServers": {
"builder-prod": { "command": "ssh", "args": ["build-host", "java", "-jar", "jvmlens.jar", "mcp"] }
} }
Path E — Kubernetes sidecar
deploy/helm/jvmlens is a standalone chart: it runs your image with the agent
attached as a separate release, so your app’s own chart is untouched.
scripts/deploy-agent.sh --release builder-profiled --namespace builder \
--target-image my-registry/builder:1.0
if the profiled copy reuses your app’s envFrom it hits the same
database — point it at a throwaway DB or run read-only. See
deploy/helm/jvmlens/README.md.
|
Scoping: make your code lead
For a foreign project the single most useful knob — restrict attribution to your
packages so framework frames don’t bury your code. Works on analyze and profile:
java -jar tools/jvmlens.jar analyze -a com.example.builder builder-run.jfr # include-only
java -jar tools/jvmlens.jar analyze -x com.thirdparty builder-run.jfr # exclude more
Both flags are repeatable and comma-separable; the MCP tools accept the same as
appPackages / exclude.
A drop-in recipe and CLAUDE.md snippet
A tiny tools/profile.sh your team and agents can run without remembering flags:
#!/usr/bin/env bash
set -euo pipefail
JVMLENS=${JVMLENS:-tools/jvmlens.jar}
PID=$(pgrep -f 'builder.jar' | head -1) || { echo "builder not running"; exit 1; }
exec java -jar "$JVMLENS" profile "$PID" -d "${DURATION:-20}" -w "${WARMUP:-3}" \
-a "${APP_PKG:-com.example.builder}" -f "${FORMAT:-prompt}"
Paste into the target project’s CLAUDE.md so its coding agents know the workflow:
## Profiling (jvmlens)
- The jar lives at `tools/jvmlens.jar` (built from github.com/alexmond/jvmlens; Java 17+).
- One-shot, running JVM: `./tools/profile.sh`.
- From a .jfr file: `java -jar tools/jvmlens.jar analyze <file.jfr> -a com.example.builder`.
- Focus a concern: add `-r cpu|memory|locks|gc`.
- Always scope with `-a com.example.builder` so our code leads, not framework frames.
- A `⚠` caveat means too few samples — record longer or under load.