docs java javascript python c / c++ c# reading reports
How to profile Java with Flight Recorder
Java Flight Recorder (JFR) ships inside every modern JDK (11+, and OpenJDK builds included — it's been free since JDK 11). It records execution samples, allocations, GC pauses, and lock contention with about 1% overhead, which means the honest answer to “can I run it in production?” is yes — that's the point. The profile of your app under real traffic beats ten synthetic benchmarks.
Option 1 — a JVM flag at startup
The zero-tooling path: tell the JVM to record from the start and write a file on exit or after a fixed duration.
# record the first two minutes after startup, then write app.jfr
java -XX:StartFlightRecording=duration=120s,filename=app.jfr -jar app.jar
# or record continuously with a bounded buffer, dump whenever you want (see jcmd below)
java -XX:StartFlightRecording=maxsize=250m,maxage=15m,name=continuous -jar app.jar
Add settings=profile to either form for the higher-detail profiling
configuration (more frequent sampling, allocation sites) — still low single-digit
overhead, and a meaningfully richer report:
java -XX:StartFlightRecording=duration=120s,settings=profile,filename=app.jfr -jar app.jar
Option 2 — jcmd against a running process
The production workhorse: attach to an already-running JVM, no restart, no flags.
jcmd # list local JVM pids
jcmd <pid> JFR.start name=profiling settings=profile
# ... let it soak under the load you care about (60–120s is plenty) ...
jcmd <pid> JFR.dump name=profiling filename=app.jfr
jcmd <pid> JFR.stop name=profiling
In a container: kubectl exec -it <pod> -- jcmd 1 JFR.start
settings=profile works the same way (the JVM is usually pid 1), then
kubectl cp the dumped file out.
In JetBrains IntelliJ IDEA
Two routes — the built-in profiler for exploring, or the JVM flag for a file you fully control.
Route A: the IntelliJ Profiler
- Open the run configuration you normally use, then instead of Run, choose Run → Profile '<your app>' with IntelliJ Profiler (also available from the run button's dropdown).
- Exercise the code path you care about, then press Stop. The profiler tool window opens with the flame graph.
- IntelliJ saves the snapshot as a
.jfrfile — find it via the Profiler tool window's recent snapshots list, or on disk in your home directory'sIdeaSnapshotsfolder.
Route B: the flag in the run configuration
- Run → Edit Configurations…, select your application.
- In VM options (Modify options → Add VM options if the field is
hidden), paste:
-XX:StartFlightRecording=duration=120s,settings=profile,filename=app.jfr - Run normally;
app.jfrappears in the working directory after two minutes (or on exit).
In VS Code
- With the Extension Pack for Java installed, open
.vscode/launch.json(Run and Debug panel → create a launch.json if you don't have one). - Add the flag to your launch configuration's
vmArgs:{ "type": "java", "name": "Launch with JFR", "request": "launch", "mainClass": "com.example.App", "vmArgs": "-XX:StartFlightRecording=duration=120s,settings=profile,filename=app.jfr" } - Launch, exercise the app, and pick up
app.jfrfrom the workspace folder. (VS Code has no built-in JFR viewer — that's what the next step is for.) - Alternatively skip launch.json entirely: run the app from the integrated terminal
and use the
jcmdflow above.
Now analyze it. Head to the flamelens
terminal, run auth <email> → verify <code>
→ scan init, and pick your .jfr. Free tier takes files up
to 10MB — for a bigger production dump, trim with
jfr assemble/maxage, or go PRO for 100MB.
Option 3 — live scan over JMX PRO
Best for a service you can reach: a Heroku/Fly/Render dyno, a staging box, a VM with a security-group rule you control. No file at all: flamelens connects to a running JVM, records a profile in place, and analyzes it. In the terminal:
scan live jvm.example.com:9010 60 --tls --user monitorRole
Two things have to be true. The JVM must expose remote JMX, and the port must be
reachable from the flamelens server — which for most production
networks means an explicit firewall rule for our egress, a bastion/tunnel, or running
flamelens self-hosted inside your network. We refuse to connect to private, loopback,
or cloud-metadata addresses, so pointing it at 10.x from the hosted
service won't work by design.
Securing the JMX port — do this before you open it
Default remote JMX is not safe to expose. The usual snippet you'll
find (authenticate=false ssl=false) gives anyone who can reach the port
full MBean access — invoking arbitrary operations, reading configuration, and, with
the right MBeans present, a path to code execution. Treat an open unauthenticated JMX
port as a shell on that host. The flags below are the minimum for anything reachable
beyond localhost:
java \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
# Pin the RMI data port to the SAME port. Without this, JMX picks a RANDOM second
# port and your firewall rule silently won't cover it (JDK 7+).
-Dcom.sun.management.jmxremote.rmi.port=9010 \
# The address clients are told to call back on — required behind NAT/containers.
-Djava.rmi.server.hostname=jvm.example.com \
\
# 1. AUTHENTICATION
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.password.file=/etc/jmx/jmxremote.password \
-Dcom.sun.management.jmxremote.access.file=/etc/jmx/jmxremote.access \
\
# 2. TLS — encrypts the connection AND the password crossing it
-Dcom.sun.management.jmxremote.ssl=true \
-Dcom.sun.management.jmxremote.registry.ssl=true \
-Dcom.sun.management.jmxremote.ssl.enabled.protocols=TLSv1.3,TLSv1.2 \
-Djavax.net.ssl.keyStore=/etc/jmx/keystore.p12 \
-Djavax.net.ssl.keyStorePassword=... \
-jar app.jar
The password file is rolename password per line and must be
chmod 600 and owned by the JVM's user — the JVM refuses to start otherwise,
which is a feature. The access file grants each role its rights:
# /etc/jmx/jmxremote.access
monitorRole readwrite
Flight Recorder control needs readwrite — starting a recording is an
operation invocation, which readonly doesn't permit. Note what
you're granting: readwrite on the platform MBeans is a powerful capability, so use a
dedicated role with a strong unique password, and deliberately omit the
create/unregister clauses (they'd let a caller register new
MBeans). Rotate the password after a vendor has used it.
Then narrow who can even reach the port: a security-group/firewall rule allowing only
the flamelens egress address, not 0.0.0.0/0. Defense in depth beats any
single control here.
On our side: pass --tls so the connection (and your
password) is encrypted, and --user <role> to authenticate — the
terminal then prompts for the password with the input masked, and it is used for that
one connection and never stored or logged. Our client validates the target's
certificate against the standard trust store with hostname verification on, so a
self-signed cert needs to be one we trust — use a real certificate for the JMX
listener. Prefer mutual TLS
(-Dcom.sun.management.jmxremote.ssl.need.client.auth=true) if your
deployment can support issuing us a client certificate.
If none of that is palatable for a production service — a very reasonable position —
you have two better options: jcmd dumps uploaded by hand, or the agent
below, which needs no exposed port at all.
Option 4 — the agent, for continuous sampling ENTERPRISE
Best for a private network: a VPC with no ingress, a locked-down k8s cluster, or anywhere the answer to “can we open a port?” is no. Everything above has flamelens dialing into your network. The agent inverts that:
- No inbound anything. No exposed JMX port, no firewall hole for our egress, no public listener to get wrong.
- Your credentials stay yours. The agent runs beside the JVM and
uses your local JMX config — usually
localhost:9010with no authentication needed at all, because it never leaves the host. flamelens never holds a secret that unlocks your production. - It samples on a schedule. That's the real unlock: a profile every 30 minutes, each one diffed against the last, so a tuning session becomes a timeline of what regressed and what your fix actually did.
Enroll one in the terminal, then set a schedule:
agent new prod-checkout-1
# → prints a token, ONCE. store it like a password.
schedule new checkout localhost:9010 java 30 60
# → sample localhost:9010 for 60s, every 30 minutes
The agent is a loop: ask what's due, record it locally with jcmd, post the
file. Nothing exotic — here it is in full, so you can read every line before running
it (it needs only bash, curl, jq, and a JDK):
#!/usr/bin/env bash
# flamelens agent. Runs beside your JVM. Connects OUT only.
set -euo pipefail
TOKEN="${FLAMELENS_AGENT_TOKEN:?set FLAMELENS_AGENT_TOKEN}"
API="https://flamelens.dev/v1/api/agent"
PID="${TARGET_PID:?set TARGET_PID to the JVM's pid}"
while true; do
work=$(curl -sf -X POST "$API/work" -H "X-Flamelens-Agent-Token: $TOKEN" || echo '{"work":[]}')
echo "$work" | jq -c '.work[]' | while read -r item; do
id=$(echo "$item" | jq -r .scheduleUuid)
secs=$(echo "$item" | jq -r .durationSeconds)
out=$(mktemp /tmp/flamelens-XXXXXX.jfr)
jcmd "$PID" JFR.start name=flamelens settings=profile duration="${secs}s" >/dev/null
sleep "$((secs + 2))"
jcmd "$PID" JFR.dump name=flamelens filename="$out" >/dev/null
jcmd "$PID" JFR.stop name=flamelens >/dev/null 2>&1 || true
curl -sf -X POST "$API/upload" \
-H "X-Flamelens-Agent-Token: $TOKEN" \
-F "scheduleUuid=$id" -F "profile=@$out" >/dev/null && echo "sampled $id"
rm -f "$out"
done
sleep 60 # check-in interval; schedules decide the real cadence
done
Each work item also carries a runtime field, so one agent can serve a
polyglot fleet — see the branching version
that also handles Node and Python.
Run it under systemd, as a sidecar container, or in a screen session — whatever fits. The token is the only secret, it only grants uploading profiles to your own account, and you can revoke it any time. Samples land in your current tuning session and appear in the terminal and report list like any other scan.
Cost note: every sample is a full analysis, so the minimum interval
is 15 minutes and schedules pause themselves after three consecutive failures (with
the reason shown in schedule) rather than retrying forever.
What makes a good recording
- Record the problem, not the startup. Start (or dump) while the slow thing is happening; 60–120 seconds under representative load is ideal.
- Prefer
settings=profilewhen overhead tolerance allows — the default configuration skips the allocation detail that makes allocation findings sharp. - One JVM per file. A recording from the service that's actually slow, not the gateway in front of it.