profile-bee supports non-intrusive Java CPU profiling on Linux by combining native stack unwinding (frame pointers or DWARF) with JIT symbol maps.
This is Phase 1 of the Java roadmap. Full HotSpot VM-struct unwinding in eBPF (OTel eBPF Profiler style) is planned as a later phase — see below.
# Profile the whole machine — probee finds running JVMs, dumps JIT maps,
# and resolves Java method names without manual jcmd:
sudo probee --tui
# Or target one PID (still auto-dumps Compiler.perfmap if needed).
# When a PID is targeted, Java auto-discovery is SCOPED to that PID only —
# no other JVMs on the host are scanned or attached to:
sudo probee --pid <pid> --tui
# Spawn a workload under the profiler. `-XX:+PreserveFramePointer` is injected
# automatically via JAVA_TOOL_OPTIONS — you do not need to pass it yourself:
sudo probee --tui -- java -jar myapp.jarCollapsed / SVG output works the same as for native apps:
sudo probee -o java.svg -t 10000| Flag | Default | Meaning |
|---|---|---|
--auto-java true|false |
true |
Dump/load JIT perf-maps for HotSpot (scoped to --pid/--cmd target; system-wide /proc scan only when no target) |
--java-refresh-secs N |
30 |
Re-dump/reload maps every N seconds (0 = off) |
Auto-discovery is scoped to the profiling target:
- With
--pid/--cmd/-- <cmd>, profile-bee only dumps and reloads the perf-map for that process. It never scans/procfor other JVMs and never sendsjcmd/SIGQUIT attach traffic to unrelated (possibly production) JVMs on the host. This also keeps startup fast — no synchronous attach to every JVM before sampling begins. - Only a truly system-wide profile (no target) scans and attaches to all JVMs.
Note that the HotSpot attach protocol (used as a fallback when jcmd is
unavailable) triggers the JVM's attach listener via SIGQUIT, which prints a
thread dump to the target's console. That fallback is therefore only ever
directed at the process you explicitly chose to profile.
Disable attach traffic entirely if you only want passive map loading:
sudo probee --auto-java false --tui| Capability | Status |
|---|---|
Detect HotSpot/libjvm.so processes |
✅ automatic |
Inject -XX:+PreserveFramePointer when spawning java |
✅ automatic |
JVM scan at startup (system-wide) / scoped to target (--pid) |
✅ automatic |
Auto-dump Compiler.perfmap (jcmd / attach / nsenter) |
✅ automatic (JDK 17+) |
Resolve JIT PCs via /tmp/perf-<pid>.map |
✅ automatic |
Demangle Ljava/lang/String;equals → java.lang.String.equals |
✅ |
Tag JVM stubs (Interpreter, …) as [jvm] … |
✅ |
| Container path + NSpid-aware map names | ✅ |
| Live reload + periodic re-dump for JIT churn | ✅ |
Native libjvm.so frames via DWARF/FP |
✅ (existing) |
| Interpreter / nmethod frame walk in eBPF | ❌ Phase 3 |
Inlined Java frames (ScopeDesc) |
❌ Phase 3 |
| Allocation / lock profiling | ❌ use async-profiler |
| JFR output | ⏳ separate roadmap item |
If automatic dump fails (permissions, exotic JVM), JIT frames may still appear
as [unknown]. profile-bee logs a one-time hint with recovery steps.
When a JVM PID is first sampled (and, for system-wide profiling, at startup), profile-bee:
- Identifies the JVM (maps
libjvm.so, or an exactjava/javawlauncher — siblings likejavac/javadocare not treated as launchers) - Looks for an existing
/tmp/perf-<pid>.map(host, NSpid, container root) - If missing, tries in order:
- host
jcmd <pid> Compiler.perfmap jcmdnext to the process’s ownjavabinary- direct HotSpot attach socket (
.java_pid*, SIGQUIT trigger) nsenter -t <pid> -m -p+ in-namespacejcmd(containers)
- host
- Loads the map into the JIT symbol cache and refreshes periodically
JDK version: the
Compiler.perfmapdiagnostic command requires JDK 17+. On JDK 8/11 it fails with "Unknown diagnostic command", so auto-dump cannot generate a map — useperf-map-agentor async-profiler to produce/tmp/perf-<pid>.map, and profile-bee will load it.-XX:+PreserveFramePointer(auto-injected on spawn) works on 8u60+.
You can still dump maps yourself; profile-bee will pick them up:
jcmd <pid> Compiler.perfmapYou can still use async-profiler to generate maps or to validate profile-bee stacks:
asprof -d 30 -f ap.html <pid> # gold-standard Java flamegraph
asprof dump-perfmap <pid> # if your build supports it-XX:+PreserveFramePointer (JDK 8u60+, usually <1% overhead) keeps RBP as a
real frame pointer in JIT-compiled code so the FP unwinder can walk mixed
native/Java stacks. When you launch the JVM through profile-bee
(probee -- java … or --cmd), it is injected automatically via
JAVA_TOOL_OPTIONS; for already-running JVMs you must have started them with
the flag. DWARF mode (--dwarf) helps for libjvm.so built without FPs.
perf_event sample
→ eBPF: FP or DWARF unwind (native IPs, including JIT PCs as raw addresses)
→ userspace TraceHandler
1. blazesym (ELF symbols for libjvm, libc, …)
2. V8 SFI path (Node only)
3. perf-map lookup for remaining [unknown] addresses ← Java Phase 1
4. format HotSpot names for flamegraphs
Relevant code:
| Module | Role |
|---|---|
profile-bee/src/java/mod.rs |
JVM detection, symbol display helpers |
profile-bee/src/java/attach.rs |
Discover JVMs, jcmd/attach/nsenter dump |
profile-bee/src/perf_map.rs |
Parse/cache /tmp/perf-<pid>.map |
profile-bee/src/trace_handler.rs |
Symbolize + JIT fallback + auto-dump |
profile-bee/src/event_loop.rs |
Startup scan + periodic refresh |
- In-process JVMTI agent; VM-struct / AGCT stack walking; best Java fidelity (inlines, interpreter, alloc/lock modes).
- profile-bee does not inject an agent. Use async-profiler when you need maximum Java detail or non-CPU modes; use profile-bee for system-wide native+JVM with a single external binary.
- Full HotSpot eBPF unwinder: code-cache segmap, interpreter frames, userspace Method* symbolization via VM structs.
- This is the blueprint for profile-bee Phase 3.
- Java APM (TLS spans via injected ByteBuddy agent) + JVM memory USDT metrics.
- Not a CPU profiler. Optional future work: read OBI’s pinned
traces_ctx_v1map to correlate profile samples with active trace/span IDs.
- Parse
gHotSpotVMStructsfromlibjvm.so(per JDK build). - Remote
process_vm_readvofMethod*/nmethod*for names when cookies are available (mirrorsv8/heap.rs).
HotspotProcInfoinprofile-bee-common+ BPF maps (parallel toV8ProcInfo).- Code-cache residency → interpreter / nmethod / stub unwind actions.
- Side-channel metadata array (parallel to
v8_sfi) with pointer + cookie. - Tail-call stepping under
perf_event(same constraints as DWARF/V8).
- OTLP/pprof frame types (
java/jvm). - JFR writer (see
docs/NEXT_STEPS.md§10d). - e2e fixtures vs async-profiler collapse output.
- Optional OBI trace correlation.
| Symptom | Fix |
|---|---|
All Java frames [unknown] |
Run jcmd <pid> Compiler.perfmap; confirm /tmp/perf-<pid>.map exists and is readable by root |
Only libjvm.so C++ symbols |
Map missing or empty; JIT not warmed up |
| Broken stacks at JIT boundaries | Try --dwarf and/or -XX:+PreserveFramePointer |
| Container PID | profile-bee also checks /proc/<pid>/root/tmp/perf-<pid>.map |
| OpenJ9 / Graal native image | Not targeted yet (HotSpot-first) |
- async-profiler Stack Walking Modes
- OTel eBPF Profiler HotSpot interpreter
- Linux
perf-map-agent/ JIT dump conventions