Profiling and JVM Diagnosis

Monitoring finds the symptom; profiling explains where resources went; benchmarking measures a repeatable workload under controlled conditions. A profile taken under a debugger or heavy instrumentation is perturbed evidence. Keep that distinction in every report.

The practical JVM approach complements the concurrency, classloading, performance and runtime grounding of The Well-Grounded Java Developer, second edition. This page is a Reaktor operating guide based on primary tool documentation, not a transcription of the book.

Start with a hypothesis

SymptomFirst evidenceFollow-up
High latency and high CPUCPU samples, GC and runnable-thread timelineAttribute hot stacks to operation and exact source
High latency and low CPUWall-time stacks, waits/locks, I/O and pool queuesDistinguish dependency delay, contention and scheduling
High allocation and frequent GCAllocation samples and GC/live-set timelineSeparate transient churn from retention
Increasing post-GC heapComparable heap/retention evidenceDominators, paths to roots, owner lifecycle
RSS grows while heap is stableNative/process accounting, buffers, threads, mappingsNMT for JVM internals; OS/native tools for uncovered allocations
Threads or handles growRepeated counts and thread/resource ownershipRepeated attach/detach, timeout, cancellation and close cases
Slow first request or startupStartup/JIT/classloading timelineKeep cold, warmed and steady-state results separate

Use instance diagnostics for the metric semantics and load tests for representative workload construction.

JVM capture workflow

Run diagnosis against the selected process with matching access and container/PID context. Discover the target JVM's supported commands with jcmd "$JVM_PID" help; capabilities vary by version. Choose an artifact directory with sufficient space and an approved retention policy.

# Set JVM_PID to the selected application PID; PROFILE_DIR is an existing directory.
jcmd "$JVM_PID" VM.version
jcmd "$JVM_PID" VM.flags
jcmd "$JVM_PID" GC.heap_info
jcmd "$JVM_PID" Thread.print -l
jcmd "$JVM_PID" JFR.start name=reaktor-diagnosis settings=profile duration=60s filename="$PROFILE_DIR/diagnosis.jfr"

JFR default settings suit ongoing lower-overhead observation; profile collects more for a bounded investigation. Inspect the recording in JMC with request/GC/thread context. Heap dumps and class histograms have higher impact and require a separate bounded capture decision. A heap dump can request a full GC; do not put it on a routine health-check path. JDK 25 jcmd reference.

For a suspected JVM-native leak, start the qualified process with -XX:NativeMemoryTracking=summary, establish a baseline and compare after the reproducing interval:

jcmd "$JVM_PID" VM.native_memory baseline
# Run the fixed reproducing workload, then:
jcmd "$JVM_PID" VM.native_memory summary.diff

NMT must be enabled at startup. It tracks HotSpot internals, not every third-party native allocation; record its overhead and pair it with process memory evidence. Reserved address space differs from committed memory. Native Memory Tracking.

Use async-profiler for the supported CPU, wall, allocation or lock question. These modes answer different questions; allocation samples do not prove retention. Pin version, platform, permissions and settings, and retain the original output. Profiling modes.

Cross-platform qualification

Android system traces/simpleperf, Apple Instruments, browser performance/heap tools and native profilers remain the specialized capture tools. Reaktor adapters normalize the capture envelope and a small summary, rather than replacing the tools or pretending their semantics match.

For Workers, do not infer CPU time by subtracting performance.now() around a tight loop: Worker clocks advance on I/O according to the runtime's documented constraints. Use supported provider CPU observations and qualify any local profiling claim separately. Worker timers.

The October FlexBuffers review now retains JVM CPU/allocation artifacts and measured host, browser and physical-device codec results. That evidence does not qualify every capture script, an FFI hop, or the book's proposed mutable engine.

The flexbuffer profiling scripts are useful source material, not a blanket five-platform qualification. The September review recorded broken orchestration and Android/iOS capture issues. Re-run each current script on its intended target before promotion; require an actual artifact per supported profile.

Capture and storage contract

Store target/activation, exact artifact and source identities, workload/base, runtime/device, build mode, timestamps, tool/settings, symbols, acquisition method, coverage and perturbation. Keep a full original artifact plus a normalized ReaktorPerformanceReport for budgets. Perfetto is useful where supported; JFR, Instruments and .cpuprofile files require explicit supported conversion, not a renamed extension.

Symbolicate against the executed artifact, not the latest build. Missing symbols and unattributed samples remain visible. Bind an on-demand capture to graph scope, duration, byte/resource budgets, cancellation, expiry and a cleanup receipt. Large dumps and captures have narrower access than aggregate metrics.

A diagnosis is complete when

The finding identifies a mechanism with supporting evidence, a fixed workload reproduces it, the candidate improves the relevant absolute/relative measure without breaking correctness, and the repeated lifecycle releases resources. Retain uncertainty and run-to-run variation. Reaktor's graph maps evidence to ownership; it does not turn sampled stacks into proof of all executions.