Production Recording Guide¶
This guide covers operating the cjfr Java agent in long-running production services: rotating files, live tuning, storage sizing, and common patterns for continuous GC profiling.
Starting a Continuous Recording¶
At JVM startup¶
java -javaagent:/opt/cjfr/cjfr.jar='start,/var/recordings/app_$index.cjfr,rotating,max-files=10,max-size=100m' \
-jar myapp.jar
Container and environment-variable deployments
In Docker containers or anywhere you can't modify the JVM command line directly,
set JAVA_TOOL_OPTIONS instead:
JAVA_TOOL_OPTIONS=-javaagent:/opt/cjfr/cjfr.jar=start,/var/rec/app_$index.cjfr,rotating,max-files=10,max-size=100m
JAVA_TOOL_OPTIONS itself; no shell quoting needed for $index.
See Container & Sidecar Deployment for a full Docker example.
Attaching to a running process¶
# by main-class substring (case-insensitive)
cjfr agent myapp start '/var/recordings/app_$index.cjfr' --rotating --max-files=10 --max-size=100m
# by PID
cjfr agent 12345 start '/var/recordings/app_$index.cjfr' --rotating --max-files=10 --max-size=100m
# all discovered JVMs at once
cjfr agent all start '/var/recordings/$index.cjfr' --rotating --max-files=5 --max-size=50m
Single-quote the path
When the output path contains $index or $date, always single-quote it
in shell to prevent expansion: '/var/recordings/app_$index.cjfr', not
"/var/recordings/app_$index.cjfr".
Rotation Knobs¶
These flags control file rotation. When used in a -javaagent: string the dashes are optional
(rotating, max-size=100m); when passed to the cjfr agent CLI they take the standard -- form
(--rotating, --max-size=100m).
| Flag | Default | Description |
|---|---|---|
rotating |
off | Enable file rotation. Requires max-size or max-duration (or both). |
max-size=<size> |
0 (unlimited) | Max size per individual file. Rotate when reached. Minimum 1024 bytes. Examples: 50m, 200m, 1g. |
max-duration=<time> |
0 (unlimited) | Max wall-clock duration per individual file. Rotate when reached. Minimum 1 ms. Examples: 5m, 1h. |
max-files=<n> |
10 | Max number of files kept. Oldest is evicted once limit is reached. Must be ≥ 1 when rotating. |
new-names |
off | If off (default): oldest file is overwritten; on-disk names are stable. If on: each rotation creates a new name; oldest file is deleted when limit reached. |
duration=<time> |
0 (unlimited) | Total cap on the whole recording (not per-file). Recording stops after this. Does not require rotating. |
rotating validation rules¶
- At least one of
max-sizeormax-durationmust be non-zero. max-durationwithoutrotatingis rejected.max-sizewithoutrotatingis rejected.- Setting both
max-size=0andmax-duration=0while rotating is rejected.
Path placeholders¶
When rotating is set, the output path should contain a placeholder:
| Placeholder | Replaced with |
|---|---|
$index |
Monotonically increasing integer (0, 1, 2, …) |
$date |
Timestamp when the file was opened (YYYY-MM-DD_HH-MM-SS-mmm, UTC) |
If neither placeholder appears in the path, .cjfr is automatically replaced
with _$index.cjfr (e.g. recording.cjfr → recording_0.cjfr, recording_1.cjfr, …).
new-names vs. default (name reuse)¶
Default (name reuse): Files are named app_0.cjfr, app_1.cjfr, …, app_9.cjfr
(for max-files=10). Once all 10 slots are used, file app_0.cjfr is overwritten
on the next rotation. Disk usage is bounded to exactly max-files × max-size.
Log-shippers that watch by filename will see the file change in-place.
new-names: Every rotation generates a new name (app_0.cjfr, app_1.cjfr,
app_2.cjfr, …). When max-files is reached, the oldest file is deleted.
Names are never reused. Log-shippers watching by inode handle this correctly,
but the file-name set grows until max-files cap is hit.
Choosing for log-shippers: Use --new-names with $date in the path if you
are shipping files with Filebeat, Fluentd, or similar tools that track files by
inode. These tools reliably ingest completed files before they are deleted, as long
as the shipper is faster than your rotation interval. For fixed-path setups where
the shipper reads by filename, use the default (name reuse) mode and ensure
max-files × max-size is large enough to cover upload delays.
Live Tuning a Running Recording¶
Limits can be changed while recording is active:
# Increase file count (useful after a disk expansion)
cjfr agent myapp set-max-files 20
# Shrink per-file size cap
cjfr agent myapp set-max-size 50m
# Change per-file duration cap
cjfr agent myapp set-max-duration 15m
# Set or shorten the total recording duration
cjfr agent myapp set-duration 4h
Constraints: set-max-files must be ≥ 1 when rotating. Setting both size and
duration to zero while rotating is rejected. These commands surface any validation
error immediately in the CLI output.
Not tunable at runtime: condenser config (--condenser-config) and JFR config
(--config) are fixed at recording start and cannot be changed.
Common Production Recipes¶
1. Rolling hot buffer; bounded disk usage¶
Keep the last 500 MB of activity at all times. Names are stable (good for fixed-path shippers):
java -javaagent:cjfr.jar='start,/var/rec/app_$index.cjfr,rotating,max-files=5,max-size=100m' \
-jar myapp.jar
Disk usage: ≤ 500 MB at all times. Oldest file is overwritten in-place on each rotation.
2. Time-sliced archive; one file per hour, 24 h retention¶
Use new-names so each file gets a unique timestamp and can be shipped independently:
java -javaagent:cjfr.jar='start,/var/rec/app_$date.cjfr,rotating,max-duration=1h,max-files=24,new-names' \
-jar myapp.jar
3. Fixed-time single-file capture¶
For a 30-minute snapshot. No rotation needed:
Or at startup:
4. Combined: total cap with time-sliced rotation¶
Record for 1 hour total, 10-minute slices, keep at most 6 files:
java -javaagent:cjfr.jar='start,/var/rec/app_$index.cjfr,rotating,max-duration=10m,max-files=6,duration=1h' \
-jar myapp.jar
5. Maximum compression for high-volume fleet¶
Smallest possible files for a fleet of busy services:
java -javaagent:cjfr.jar='start,/var/rec/app_$index.cjfr,rotating,max-files=10,max-size=50m,condenser-config=reduced' \
-jar myapp.jar
Use the platform-inflaterless-minimal JAR (~450 KB) for the smallest possible
agent footprint. The .cjfr files are still readable by any full-size JAR offline.
Squeeze further at archival time
The agent records with fast compression to keep write overhead low. When you later move recordings to cold storage, re-condense them offline for the smallest files:
Checking Recording Status¶
Returns a table showing: config, jfr-config, start time, elapsed time, current file size (compressed and uncompressed), number of files, whether rotation is active, and event-error count.
Storage Sizing¶
These figures are the output .cjfr file size, not the input JFR size.
Measured on renaissance gc_details benchmarks with LZ4FRAMED compression (the default).
Actual sizes depend heavily on workload type; sparse gc-only profiles produce much smaller files.
| Condenser config | .cjfr output MB/hour (gc_details-heavy) |
.cjfr output MB/hour (gc-only sparse) |
|---|---|---|
lossless |
~300 MB/hour | ~25 MB/hour |
default (agent default) |
~130 MB/hour | ~10 MB/hour |
reduced |
~70 MB/hour | ~6 MB/hour |
Based on a 7m52s renaissance benchmark where the equivalent raw JFR was ~242 MB (~1.8 GB/hour). For gc-only sparse profiles (renaissance-all_gc_G1.jfr, 29 MB input), output is roughly 10% of the gc_details rate. Actual results depend on GC frequency, thread count, and allocation rate.
Tuning JFR Event Coverage¶
The condenser config controls event reduction. The JFR configuration controls which events are captured and at what overhead. These are independent: the condenser reduces whatever JFR captured; it cannot add events that JFR didn't record.
| Flag | Controls |
|---|---|
condenser-config |
How aggressively events are reduced/combined |
config |
Which JFR event set to capture (default, profile, or a custom .jfc path); also controls runtime overhead |
To use JFR's profile config (more events, higher overhead — CPU samples, allocation events) with cjfr's default reduction:
java -javaagent:cjfr.jar='start,/var/rec/app.cjfr,config=profile,condenser-config=default' \
-jar myapp.jar
To override specific JFR event intervals (e.g. reduce CPU sample frequency):
java -javaagent:cjfr.jar='start,/var/rec/app.cjfr,misc-jfr-config=jfr.ExecutionSample#interval=100ms' \
-jar myapp.jar
misc-jfr-config takes |-separated EventName#setting=value pairs:
GC-Log Replacement Mode¶
Replace -Xlog:gc* unified GC logging with a CJFR recording at near-zero overhead. The gc-log preset captures the equivalent of gc+heap+cpu+metaspace+ref+phases+promotion+ergo+age at a fraction of the storage cost — and adds structured data, nanosecond timestamps, and ambient system context that the GC log cannot provide.
Quick start¶
java -javaagent:cjfr.jar='start,/var/rec/gc_$index.cjfr,rotating,max-files=24,max-duration=1h,config=gc-log,condenser-config=gc-log' \
-jar myapp.jar
What it captures¶
GC events (all collectors): GarbageCollection, GCPhasePause, GCPhaseConcurrent and sub-phases, GCHeapSummary, GCCPUTime, MetaspaceSummary, GCReferenceStatistics, PromotionFailed, ConcurrentModeFailure.
Collector-specific: - G1GC: G1GarbageCollection, G1HeapSummary, TenuringDistribution, G1MMU, G1BasicIHOP, G1AdaptiveIHOP, EvacuationInformation, EvacuationFailed, G1EvacuationYoung/OldStatistics - ZGC: ZYoungGarbageCollection, ZOldGarbageCollection, ZAllocationStall, ZPageAllocation, ZRelocationSet, ZRelocationSetGroup, ZUncommit - Shenandoah: ShenandoahHeapRegionInformation (sampled, everyChunk) - Parallel GC: PSHeapSummary
Ambient context: CPULoad (1 s), PhysicalMemory, ResidentSetSize, SwapSpace, OSInformation, CPUInformation, VirtualizationInformation, ContainerConfiguration/CPUUsage/CPUThrottling/MemoryUsage/IOUsage (30 s), JVM flags (all 7 primitive flag types + change events), NativeMemoryUsage/Total (1 s), DirectBufferStatistics, FinalizerStatistics, GCLocker (≥1 s), CodeCacheFull, ThreadContextSwitchRate (10 s), ExecuteVMOperation (≥10 ms).
Not captured (no JFR events exist for these GC log tags): gc+refine, gc+remset, gc+humongous summary counts, gc+stringdedup / stringdedup (no jdk.StringDeduplication event exists in the JDK — the stats are text-log-only), ZGC gc+mmu. These are confirmed gaps — see the gc-log research notes for proposed upstream JFR events that would close them.
Storage estimates¶
Measured on Linux (OpenJDK 21.0.11, 256 MB heap, constant 32 KB allocation workload, 60 s runs, 3 runs per collector, run-to-run variance <1%). Sizes are per 60 s run:
| Collector | -Xlog:gc* text |
Raw JFR (gc-monitoring.jfc) | gc-log CJFR |
CJFR vs Xlog |
|---|---|---|---|---|
| G1GC | 541 KB | 597 KB | 183 KB | 66% smaller |
| ZGC | 311 KB | 300 KB | 93 KB | 70% smaller |
| Shenandoah | 811 KB | 428 KB | 121 KB | 85% smaller |
| ParallelGC | 119 KB | 336 KB | 104 KB | 13% smaller |
Workload allocates far harder than typical production (production GC rates are usually 10–100× lower), so absolute sizes are worst-case. The CJFR-vs-Xlog margin tracks how verbose the text log is: Shenandoah's text log is the chattiest (biggest win); ParallelGC's is very terse (smallest win). The raw JFR is often larger than the text log — condensing is what produces the net win.
The gc-log CJFR output is 66–85% smaller than uncompressed -Xlog:gc* text on G1/ZGC/Shenandoah,
and 13% smaller on the terse ParallelGC log — while supporting random-access structured queries that text
logs cannot.
Honest caveat — CJFR vs gzipped text: CJFR is LZ4-compressed; the ratios above compare against raw
text (what sits on disk between log rotations). A gzip -9'd GC log can be smaller than CJFR (on a 20 s
G1 sample: 849 KB gzipped text vs 2.35 MB CJFR). The case for CJFR is not smallest-bytes-on-disk — it
is structured field-addressable access (cjfr view, JMC, Firefox Profiler), ns-precision timings, and
correlated ambient events (CPU, RSS, container limits, flags) that the text log omits entirely.
Why JFR over -Xlog:gc*¶
| What you need | -Xlog:gc* |
cjfr gc-log preset |
|---|---|---|
| GC pause times (ns precision) | ms only | ns |
| Heap before/after each GC | yes | yes |
| GC type, cause, and collector | yes | yes |
| Concurrent phase timings | yes | yes |
| G1 IHOP decisions | yes (info level) | yes + model state |
| G1 MMU | yes | yes |
| CPU time per GC (user/sys/real) | yes | yes |
| Metaspace before/after | yes | yes |
| Tenuring distribution (age buckets) | requires -Xlog:gc+age=debug |
yes |
| G1 pause sub-phases (level 1+2) | -Xlog:gc+phases=debug |
yes |
| G1 evacuation statistics | partial (ergo level) | yes (structured fields) |
| ZGC relocation set breakdown | yes (large text tables) | yes (structured, 95% smaller) |
| JIT code cache overflow (affects pauses) | no | yes (CodeCacheFull) |
| OS context switch rate (pause spikes) | no | yes (ThreadContextSwitchRate) |
| Container CPU/memory limits | no | yes |
| JVM flag set at startup | no | yes |
| Overall JVM CPU load (1 s) | no | yes |
| Resident set size / swap pressure | no | yes |
| Bounded rotating files (compressed) | separate logrotate | built-in |
| Structured query (field access by name) | grep/regex only | yes |
| Crash recovery (chunked writes) | may truncate | yes |
| G1 refinement thread activity | -Xlog:gc+refine=debug (high-volume text) |
gap — no JFR event |
| String deduplication stats | -Xlog:gc+stringdedup=info (separate subsystem) |
gap — no jdk.StringDeduplication event exists in the JDK (text-log-only) |
| Humongous reclaim counts | -Xlog:gc+humongous=debug |
gap — no JFR event |
| ZGC director heuristics | -Xlog:gc+director=debug |
gap — ZAllocationStall covers blocked threads but not trigger decisions |