JFR e JMC — observabilidade de produção

TL;DR

JFR (Java Flight Recorder) é um gravador de eventos embutido na JVM — GC, alocação, locks/monitor, I/O, CPU — que pode ficar ligado o tempo todo em produção porque seu overhead é baixo o suficiente para uso contínuo (o profile default.jfc tem impacto mínimo de performance, conforme a documentação oficial). JMC (JDK Mission Control) é a ferramenta de análise dos arquivos .jfr: projeto separado, download independente do JDK, interface visual com automated analysis, method profiling e visões de latência. A metáfora é a caixa-preta de avião: quando o incidente acontece — CPU explodindo, latência de lock oculta, alocação excessiva antes do GC — a gravação já existia. O forense reativo (nota 12: heap dump, thread dump) captura a foto depois do sinistro; o JFR grava o filme antes.

O que é

JFR (Java Flight Recorder) é um subsistema de coleta de eventos embutido no HotSpot JVM, disponível no JDK desde o Java 11 como software open source (JEP 328, integrado ao OpenJDK). Não é um agente externo nem um profiler de attach: é parte da JVM, o que explica o overhead estruturalmente baixo.

O JFR registra eventos em buffers em memória, organizados em chunks escritos periodicamente em disco quando disk=true. Cada evento tem timestamp, duração (para eventos de duração) e campos de payload tipados. O resultado final é um arquivo .jfr — um formato binário eficiente que os analisadores leem sem precisar do processo original.

JMC (JDK Mission Control) é a ferramenta de análise desses arquivos. Desde o Java 11 também é open source (projeto Eclipse Mission Control), mas é distribuído separadamente do JDK — precisa de download independente (disponível em jdk.java.net/jmc/ ou via empacotadores como Adoptium/Azul/Red Hat). A versão estável mais recente é a 9.x (2025). Não há JMC incluído no jdk ou jre instalado.

FerramentaPapelOnde vive
JFRColeta eventos na JVM em execuçãoEmbutido no JDK (Java 11+)
JMCAnalisa arquivos .jfr offlineDownload separado
jcmd JFR.*Controla gravações ao vivoEmbutido no JDK
-XX:StartFlightRecordingLiga JFR no startupFlag da JVM

Por que importa

O JFR é um dos recursos mais subutilizados do ecossistema Java. A maioria dos engenheiros só o descobre quando precisa — ou seja, quando o incidente já aconteceu e não havia gravação. O problema é que o JFR resolve exatamente a classe de problemas que um dump pontual não pega: o que aconteceu nos 30 minutos antes do spike de CPU? Qual lock estava gerando contenção durante o horário de pico de ontem à noite? Quais objetos eram alocados em excesso antes do GC full às 3h?

Heap dump e thread dump (nota 12) são forense reativo: capturam uma fotografia de um instante, e só depois do sintoma aparecer. JFR é always-on: a gravação existe antes do incidente, e pode ser extraída retroativamente com jcmd JFR.dump.

Do ponto de vista de carreira, saber configurar JFR para gravação contínua em produção e interpretar um .jfr no JMC é um marcador claro de senioridade real — especialmente em entrevistas focadas em sistemas de alta disponibilidade e diagnóstico de performance.

Como funciona

Arquitetura de eventos (categorias, buffers, por que o overhead é baixo)

O JFR organiza seus eventos em categorias funcionais:

  • GC — coletas (tipo, duração, pausa, fases do G1/ZGC/Shenandoah), alocações que falharam no TLAB, referências
  • Alocação — alocações amostradas — em novo TLAB e fora do TLAB (jdk.ObjectAllocationInNewTLAB, jdk.ObjectAllocationOutsideTLAB), live objects
  • Locks / monitor — contenção em synchronized (jdk.JavaMonitorWait, jdk.JavaMonitorEnter), threads park/unpark
  • I/O — leituras e escritas em socket e arquivo com duração acima de threshold configurável
  • CPU / método — amostras de CPU com stack trace (method profiling), tempo de execução de métodos

O overhead é baixo porque o JFR não intercepta cada operação em tempo real: ele usa buffers locais por thread (thread-local buffers) que são escritos assincronamente, evita sincronização no caminho crítico, e coleta a maioria dos dados aproveitando pontos de instrumentação já existentes na JVM (GC hooks, safepoints). A documentação Oracle descreve o default.jfc como tendo “low overhead” com “minimal performance impact” — o suficiente para uso contínuo em produção. O profile profile.jfc coleta mais dados (stack traces mais frequentes, thresholds mais baixos) com overhead maior, indicado apenas para sessões curtas de diagnóstico.

Overhead — ausência de número oficial

A documentação Oracle do Java 21 (java man page, Troubleshooting Guide, jcmd man page) não publica um percentual específico de overhead para o JFR com default.jfc. O que está documentado é a qualificação “low overhead” / “minimal performance impact” para uso contínuo. A cifra de “~1-2%” frequentemente citada em blogs e apresentações vem de benchmarks comunitários, não de uma garantia da especificação. Use o hedging correto: overhead baixo o suficiente para produção com default.jfc; meça no seu workload específico antes de assegurar um número.

Ligando — -XX:StartFlightRecording e jcmd JFR.*

Via flag de startup (gravação contínua — o padrão para produção):

Opções confirmadas na java man page do Java 21:

java \
  -XX:StartFlightRecording=disk=true,maxage=12h,maxsize=512m,dumponexit=true,filename=/tmp/app-%p-%t.jfr,settings=default \
  -jar app.jar
OpçãoDefaultDescrição
disktrueEscreve dados em disco durante a gravação (necessário para ring buffer)
maxage0s (ilimitado)Tempo máximo de dados retidos em disco; 0s = sem limite
maxsize0 (ilimitado)Tamanho máximo em disco; 0 = sem limite
dumponexitfalseSalva o arquivo quando a JVM encerra
filenameauto-geradoCaminho do arquivo; suporta %p (PID) e %t (timestamp)
settingsdefault.jfcProfile: default (always-on) ou profile (diagnóstico curto)
delay0sAguarda antes de iniciar a gravação
duration0s (ilimitado)Duração da gravação; 0s = para sempre
nameauto-geradoNome da gravação (usado nos comandos jcmd)
path-to-gc-rootsfalseColeta caminho até raízes GC ao final (útil para leak, custoso)

Via jcmd ao vivo (confirmado na jcmd man page do Java 21):

# Iniciar uma gravação nomeada
jcmd <pid> JFR.start name=prod-recording disk=true maxage=6h maxsize=256m settings=default
 
# Verificar gravações em andamento
jcmd <pid> JFR.check
 
# Extrair snapshot sem parar a gravação (the killer feature)
jcmd <pid> JFR.dump name=prod-recording filename=/tmp/incident.jfr
 
# Parar e salvar
jcmd <pid> JFR.stop name=prod-recording filename=/tmp/final.jfr

JFR.dump não interrompe a gravação

Ao contrário do heap dump (que para o mundo enquanto percorre o heap), JFR.dump extrai os dados acumulados no ring buffer para um arquivo sem pausar a JVM e sem encerrar a gravação. É o equivalente a “tirar uma cópia da caixa-preta sem pousar o avião”.

Analisando no JMC

O JMC abre arquivos .jfr e oferece:

  • Automated Analysis — relatório automático que destaca os problemas mais relevantes na gravação: GC pressure, thread contention, allocation hot spots, I/O lento. Ponto de entrada obrigatório antes de mergulhar nos dados brutos.
  • Method Profiling — visão de flame graph / call tree dos stack traces amostrados. Mostra onde a CPU foi gasta. Permite filtrar por thread, por período, por pacote.
  • Allocation view — mostra quais tipos de objetos foram alocados com maior frequência e quais stack traces originaram as alocações. Complementa o heap dump: o dump diz o que está na memória agora; o JFR diz o que foi alocado ao longo do tempo.
  • Lock / Monitor view — threads que esperaram mais tempo por monitors, com stack trace de quem segurava o lock. Diagnóstico de contenção que nem heap dump nem thread dump isolados revelam bem.
  • I/O view — operações de socket e arquivo com latência acima do threshold, agrupadas por host/endpoint.

JFR Event Streaming — consumindo eventos em tempo real (JEP 349, Java 14)

Antes do Java 14, a única forma de consumir dados do JFR era abrir um arquivo .jfr offline. O JEP 349 (Java 14) introduziu o JFR Event Streaming: a capacidade de consumir eventos do JFR em tempo real, enquanto a JVM está rodando, via RecordingStream (pacote jdk.jfr.consumer).

// hipotético: monitoring de CPU e GC em tempo real
// RecordingStream como campo de instância — mantido vivo enquanto o serviço roda;
// fechado no shutdown hook para liberar o stream com segurança.
import jdk.jfr.consumer.RecordingStream;
import java.time.Duration;
 
// Em código real: campo de instância, não try-with-resources local
// (try-with-resources fecha o stream ao sair do bloco, encerrando o consumo)
var rs = new RecordingStream();
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(5));
rs.enable("jdk.GarbageCollection");
rs.onEvent("jdk.CPULoad", event -> {
    double jvmUser = event.getDouble("jvmUser");
    if (jvmUser > 0.80) {
        System.out.printf("ALERTA: CPU JVM em %.0f%%%n", jvmUser * 100);
    }
});
rs.onEvent("jdk.GarbageCollection", event -> {
    System.out.printf("GC %s — duração: %s%n",
        event.getString("cause"), event.getDuration("duration"));
});
rs.startAsync();   // processa em thread separada — a aplicação continua rodando
 
// fechar no shutdown da aplicação:
// Runtime.getRuntime().addShutdownHook(new Thread(rs::close));

RecordingStream implementa AutoCloseable e EventStream. Métodos principais:

  • onEvent(String nome, Consumer<RecordedEvent>) — registra handler por nome de evento
  • startAsync() — inicia o consumo em thread separada (a JVM não bloqueia)
  • start() — inicia no thread corrente (bloqueia até o stream ser fechado)
  • enable(String nome).withPeriod(Duration) — ativa evento periódico com intervalo
  • stop() / close() — encerra o stream

Casos de uso: dashboards internos de saúde, alertas customizados sem agente externo, testes de integração que verificam alocações ou uso de socket.

Custom events — instrumentando o domínio da aplicação

O JFR permite criar eventos de domínio próprio estendendo jdk.jfr.Event. O framework os trata exatamente como eventos nativos: aparecem no JMC, podem ser filtrados, têm timestamp e duração automáticos.

// hipotético: evento de processamento de pedido
import jdk.jfr.Category;
import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
 
@Name("com.example.OrderProcessed")
@Label("Order Processed")
@Description("Registra a conclusão do processamento de um pedido")
@Category({"Application", "Orders"})
public class OrderProcessedEvent extends Event {
 
    @Label("Order ID")
    public String orderId;
 
    @Label("Total Items")
    public int totalItems;
 
    @Label("Success")
    public boolean success;
}
 
// uso no código de produção:
var event = new OrderProcessedEvent();
event.begin();
try {
    // processamento real aqui
    event.orderId = order.getId();
    event.totalItems = order.getItems().size();
    event.success = true;
} finally {
    event.commit();   // grava o evento com duração calculada automaticamente
}

O event.shouldCommit() — verificar antes de coletar dados caros:

var event = new OrderProcessedEvent();
event.begin();
// ... processamento ...
if (event.shouldCommit()) {
    event.orderId = collectExpensiveMetadata(); // só executa se o evento for gravado
    event.commit();
}

Posicionamento — JFR vs profiler de attach vs APM

FerramentaEscopoOverheadUse case principal
JFR (default.jfc)Uma JVM, always-onBaixoGravação contínua em produção
JFR (profile.jfc)Uma JVM, sessão curtaMédio-altoDiagnóstico de CPU/alocação pontual
Profiler de attach (async-profiler, YourKit)Uma JVM, sessão curtaVariávelFlame graph detalhado, análise local
APM (Datadog, New Relic, Dynatrace)Sistema distribuídoBaixo-médioRastreamento distribuído, negócio, alertas

JFR e APM são complementares, não substitutos: o JFR enxerga o interior de uma única JVM com altíssimo detalhe; o APM enxerga o fluxo entre serviços, SLAs de negócio, e correlação de traces distribuídos. O JFR não tem context propagation entre serviços.

Na prática

Configuração de produção — ring buffer contínuo:

java \
  -XX:StartFlightRecording=disk=true,maxage=12h,maxsize=512m,dumponexit=true,settings=default \
  -jar app.jar

Com essa configuração: os últimas 12 horas de dados são mantidos em disco (ring buffer — os dados mais antigos são descartados conforme o limite é atingido); no shutdown, o arquivo é salvo automaticamente.

Dump sob demanda após incidente:

# extrair os dados do ring buffer sem parar a gravação
jcmd <pid> JFR.dump filename=/tmp/incident-$(date +%Y%m%d-%H%M%S).jfr
 
# verificar o que está gravando
jcmd <pid> JFR.check verbose=true

Custom event completo — OrderProcessedEvent:

// hipotético: evento de domínio para rastrear processamento de pedidos no JFR
import jdk.jfr.Category;
import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
 
@Name("com.example.order.OrderProcessed")
@Label("Order Processed")
@Description("Duração e resultado do processamento de um pedido")
@Category({"Application", "Orders"})
public class OrderProcessedEvent extends Event {
 
    @Label("Order ID")
    public String orderId;
 
    @Label("Item Count")
    public int itemCount;
 
    @Label("Success")
    public boolean success;
 
    @Label("Failure Reason")
    public String failureReason;
}
 
// uso:
public void processOrder(Order order) {
    var event = new OrderProcessedEvent();
    event.begin();
    try {
        doProcess(order);
        event.success = true;
    } catch (Exception e) {
        event.success = false;
        event.failureReason = e.getClass().getSimpleName();
        throw e;
    } finally {
        event.orderId = order.getId();
        event.itemCount = order.getItems().size();
        event.commit();
    }
}

Armadilhas

(1) Confundir JFR (coleta) com JMC (análise) — e tentar instalar o JMC pelo JDK

O problema: JFR vem embutido no JDK 11+, sem necessidade de instalação adicional. JMC não faz parte do JDK — não existe jmc no $JAVA_HOME/bin de uma instalação padrão. Tentar usar JMC sem instalá-lo separadamente resulta em “command not found” ou na confusão de abrir arquivos .jfr sem ferramenta disponível.

# hipotético: erro comum — procurar jmc no JAVA_HOME
ls $JAVA_HOME/bin/jmc
# ls: /usr/lib/jvm/java-21-openjdk/bin/jmc: No such file or directory

Fix: baixar o JMC separadamente em jdk.java.net/jmc/ (projeto Eclipse Mission Control) ou via empacotadores. No JDK só existem as ferramentas de coleta (jcmd JFR.*, -XX:StartFlightRecording); a análise requer o download à parte.


(2) Gravar sem maxage nem maxsize — o disco enche silenciosamente

O problema: com disk=true e maxage=0s,maxsize=0 (ambos os defaults), o JFR escreve dados em disco indefinidamente. Em aplicações de longa duração (dias, semanas), isso significa que o volume de gravação cresce até encher o disco disponível — o que pode causar falha de escrita de logs, falha de criação de heap dump, ou até queda da aplicação.

# hipotético: verificar o espaço após semanas sem configurar limites
du -sh /tmp/*.jfr
# 47G    /tmp/app-47-1234567890.jfr

Fix: sempre configurar pelo menos um dos dois limites. Para gravação contínua em produção, maxage é o mais intuitivo:

-XX:StartFlightRecording=disk=true,maxage=12h,maxsize=512m,settings=default

O JFR usa os dois limites simultaneamente: o dado é descartado quando excede maxage ou maxsize, o que ocorrer primeiro — comportamento de ring buffer.


(3) Tratar JFR como substituto de APM ou tracing distribuído

O problema: JFR enxerga o interior de uma única JVM. Ele não tem nenhum conceito de trace distribuído, correlation ID entre serviços, ou visibilidade de latência de rede entre microsserviços. Usar JFR como ferramenta principal de observabilidade em arquitetura de microsserviços resulta em pontos cegos: a latência da chamada HTTP entre serviço A e serviço B não aparece no JFR do serviço A como um evento identificável — aparece apenas como tempo de I/O de socket.

Fix: JFR e APM são complementares. O APM (Datadog, New Relic, OpenTelemetry) cuida de tracing distribuído, SLAs de negócio, correlação entre serviços. O JFR cuida do detalhe interno da JVM: alocações, GC, contenção de lock, CPU por método. Em produção, use os dois.


(4) Usar settings=profile como gravação contínua achando que é o default

O problema: o profile profile.jfc coleta stack traces com muito mais frequência, habilita eventos com thresholds mais baixos (I/O, locks), e pode ter overhead significativamente maior que o default.jfc. A documentação Oracle descreve o profile.jfc como indicado para “short periods” — não para uso contínuo. Confundir os dois e deixar settings=profile em produção pode degradar a performance da aplicação de forma perceptível.

# ERRADO para always-on — profile é para sessões curtas de diagnóstico
-XX:StartFlightRecording=disk=true,maxage=12h,settings=profile
 
# CORRETO para always-on
-XX:StartFlightRecording=disk=true,maxage=12h,maxsize=512m,settings=default

Fix: default.jfc para gravação contínua em produção. profile.jfc apenas para sessões curtas de diagnóstico de CPU ou alocação — e com duration limitado.

Em entrevista

Frase pronta (inglês)

“My default for production services is running JFR in continuous mode — disk=true, maxage=12h, maxsize=512m, settings=default. The overhead with the default profile is low enough that Oracle documents it for continuous production use. When an incident happens — CPU spike, latency degradation, GC pressure — I dump the ring buffer with jcmd JFR.dump without stopping the recording, open the file in JMC, and let the Automated Analysis surface the top issues. From there I drill into Method Profiling for CPU, the Allocation view for memory pressure, and the Lock view for thread contention — all of which happened before the incident, not just at the moment I took the snapshot.”

“The key difference from reactive diagnostics is timing. A heap dump or thread dump tells you what the JVM looks like right now. JFR tells you what happened over the last 12 hours. If the OOM or the CPU spike is a symptom of something that built up over time — a growing allocation rate, increasing lock contention, a GC that started taking longer — the JFR recording has the evidence; the point-in-time snapshot doesn’t.”

“JFR and APM are complementary, not alternatives. JFR gives deep visibility into a single JVM — object allocation, GC phases, monitor contention, method-level CPU usage. APM gives distributed tracing, cross-service latency, and business SLAs. In a microservices system you need both: JFR to understand what’s happening inside each JVM, and your APM to understand what’s happening between services.”

Vocabulário

Termo PTTermo EN
gravador de voo JavaJava Flight Recorder (JFR)
controle de missão JDKJDK Mission Control (JMC)
análise automatizadaautomated analysis
perfil / profile de coletarecording profile / configuration (default.jfc, profile.jfc)
buffer circular / anelring buffer
extração sob demandaon-demand dump (JFR.dump)
streaming de eventosevent streaming (RecordingStream)
evento de domínio customizadocustom domain event (extends Event)
contenção de monitormonitor contention / lock contention
overhead de produçãoproduction overhead

Veja também

Referências

  • JDK jcmd man page — Java 21 (Oracle) — JFR.start (parâmetros disk, maxage, maxsize, dumponexit, filename, name, settings, delay, duration, path-to-gc-roots), JFR.dump, JFR.stop, JFR.check — todos os parâmetros confirmados nessa fonte
  • java man page — Java 21 (Oracle)-XX:StartFlightRecording com subopções disk, maxage, maxsize, dumponexit, filename, settings, delay, duration, name, path-to-gc-roots — confirmadas; default.jfc descrito como “low overhead, minimal performance impact”; profile.jfc descrito como “more data, more overhead, short periods”
  • jdk.jfr package-summary — Java 21 Javadoc (Oracle) — classes confirmadas: Event, FlightRecorder, Recording, RecordingState, EventFactory, Configuration; anotações: @Label, @Description, @Category, @Enabled, @StackTrace, @Threshold, @Period; métodos begin(), commit(), shouldCommit()
  • RecordingStream — Java 21 Javadoc (Oracle) — confirmado “Since: 14” (Java 14); métodos onEvent, startAsync, start, stop, close, enable, setMaxAge, dump
  • Java Platform SE Troubleshooting Guide — Java 21 (Oracle) — overhead descrito como “very small performance overhead, so it can be used in production environments”; “can record a large amount of data on production systems while keeping the overhead of the recording process low” — sem percentual específico publicado
  • Flight Recorder API Programmer’s Guide — Java 21 (Oracle) — guia oficial da API JFR
  • Eclipse Mission Control (Adoptium) — JMC como download separado; versão estável 9.1.2 (2025); disponível para Windows/macOS/Linux x86_64 e aarch64
  • JEP 328 — Flight Recorder (Java 11) — JFR open source integrado ao OpenJDK; fonte: especificação oficial do JEP
  • JEP 349 — JFR Event Streaming (Java 14) — RecordingStream para consumo de eventos em tempo real; fonte: especificação oficial do JEP