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

  1. 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).
  2. Exercise the code path you care about, then press Stop. The profiler tool window opens with the flame graph.
  3. IntelliJ saves the snapshot as a .jfr file — find it via the Profiler tool window's recent snapshots list, or on disk in your home directory's IdeaSnapshots folder.

Route B: the flag in the run configuration

  1. Run → Edit Configurations…, select your application.
  2. In VM options (Modify options → Add VM options if the field is hidden), paste:
    -XX:StartFlightRecording=duration=120s,settings=profile,filename=app.jfr
  3. Run normally; app.jfr appears in the working directory after two minutes (or on exit).

In VS Code

  1. 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).
  2. 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"
    }
  3. Launch, exercise the app, and pick up app.jfr from the workspace folder. (VS Code has no built-in JFR viewer — that's what the next step is for.)
  4. Alternatively skip launch.json entirely: run the app from the integrated terminal and use the jcmd flow 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:

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