Em 2022, quando alguém me perguntava “compila pra native ou roda na JVM?”, a resposta era curta: depende do quanto você precisa de cold start abaixo de 100ms. Quem não precisava, ficava na JVM. Quem precisava, ia pro GraalVM Native Image e pagava o preço de reflection metadata, dynamic class loading, e tooling ainda em amadurecimento.
Quatro anos depois, a pergunta mudou. E quem ainda está decidindo native vs JVM com base em “qual é mais rápido no benchmark X” está fazendo a pergunta errada.
O que mudou: Project Leyden saiu do papel
Project Leyden, que em 2022 ainda era apresentação de keynote, virou release. O projeto entregou quatro JEPs ao longo dos JDKs 24, 25 e 26:
- JEP 483: Ahead-of-Time Class Loading & Linking (JDK 24)
- JEP 514: Ahead-of-Time Command-Line Ergonomics (JDK 25)
- JEP 515: Ahead-of-Time Method Profiling (JDK 25)
- JEP 516: Ahead-of-Time Object Caching with Any GC (JDK 26)
A motivação está literalmente escrita no JEP 516:
“the Spring PetClinic demo application starts 41% more quickly in production because the cache enables some 21,000 classes to appear already loaded and linked when the application starts.”
Esses 41% são o ganho que o AOT cache (JEP 483) entrega em produção. Sem reescrever, sem reconfigurar reachability metadata. Você roda um training run, gera o cache AOT, e o startup em produção fica mais curto. Esse é o ponto.
O que o JEP 516, que entrou no JDK 26 (GA em 17 de março de 2026), fez foi remover o último impeditivo operacional do AOT cache: agora ele funciona com qualquer garbage collector, incluindo o ZGC. Antes, quem queria tail latency baixa via ZGC tinha que abrir mão do cache AOT. Hoje não tem mais esse trade-off.
Onde está o Native Image
Native Image continua sendo o que sempre foi: AOT verdadeiro, binário standalone, sub-100ms de boot, footprint de memória bem menor que a JVM. Continua sendo a opção certa pra função serverless, scale-to-zero, container minúsculo, edge computing.
Mas tem um detalhe que muita gente passa batido: PGO (Profile-Guided Optimization), que é o mecanismo do GraalVM pra fechar parte do gap de runtime perf entre AOT e JVM, não está disponível na GraalVM Community Edition. A documentação oficial é direta:
“Note: PGO is not available in GraalVM Community Edition.”
PGO está no Oracle GraalVM, que desde junho de 2023 é gratuito para uso em produção sob a licença GFTC. Isso muda o cálculo: se a estratégia da equipe envolve PGO pra fechar o gap de throughput, é Oracle GraalVM. Pra quem está rodando OpenJDK puro com builds da comunidade, PGO não entra na conta.
E o resto continua valendo: reachability metadata pra reflection, o GraalVM Native Image agent pra capturar config em testes de integração (via -Dquarkus.test.integration-test-profile=test-with-native-agent), e depois -Dquarkus.native.agent-configuration-apply para aplicá-la no build nativo (em projetos Quarkus), e a disciplina de saber o que sua aplicação faz em runtime pra que o compilador AOT possa fazer o trabalho.
Como decidir entre os dois (sem benchmark de marketing)
O critério não é mais “qual é mais rápido em microbench X”. É de plataforma:
Vai pra Native Image quando:
- A workload é serverless ou scale-to-zero e cold start abaixo de 100ms é requisito de SLA.
- Footprint de container importa mais do que throughput de longo prazo (edge, FaaS, sidecar).
- A aplicação tem reflection contida e reachability metadata estável (LangChain4j, Quarkus extensions, frameworks com suporte de primeira classe ao Native Image).
- A equipe consegue rodar Oracle GraalVM em produção e tem capacidade de implementar PGO com workload representativa.
Fica na JVM com Leyden quando:
- A workload é long-running e o que importa é peak throughput, não cold start.
- A aplicação depende de bibliotecas que fazem dynamic class loading pesado, agentes Java em runtime, ou hot reload em desenvolvimento.
- Você quer ganho mensurável de cold start (esses 41% do PetClinic, por exemplo) sem reescrever nada e sem mudar de GC.
- A equipe está em OpenJDK comunidade e prefere ficar lá.
Os dois mundos não são mutuamente exclusivos. Nos times Java enterprise que acompanho, muitas equipes mantêm o serviço principal em JVM com AOT cache do Leyden e empurram cargas específicas (funções, jobs efêmeros, edge handlers) pra Native Image. A decisão é por workload, não por toda a stack.
O detalhe que ninguém comenta: training run
Tanto Native Image (com PGO) quanto Leyden (com AOT cache) dependem de um training run. Esse training run precisa ser representativo da carga de produção; senão o que você está otimizando é o caminho errado.
Em Native Image PGO, a doc oficial é explícita:
“the goal is to gather profiles on workload that match the production workloads as much as possible. The gold standard for this is to run the exact same workloads you expect to run in production on the instrumented binary.”
Em Leyden, o training run roda numa JVM regular com flag específica e produz o cache AOT com classes pré-carregadas e profiles. Em ambos os casos, se o training run for um teste sintético pobre, o ganho em produção vai ser igualmente pobre. E em alguns casos pode ser pior do que não ter feito nada.
Pra quem está saindo de stage 1 (testes locais) pra stage 2 (CI/CD), isso significa investir em workload de produção replicável: traffic shadowing, replay de payloads anonimizados, testes de carga que de fato exercitam os caminhos quentes. É trabalho de plataforma, não de feature.
Aplicação prática em projeto Java enterprise
Cenário concreto que costumo ver em sistemas enterprise: aplicação Quarkus rodando em Kubernetes, cargas mistas de REST síncrono + jobs em background + alguns endpoints que viram função sob carga elevada.
Estratégia que faz sentido:
- Núcleo da aplicação: Quarkus em JVM (OpenJDK 26+) com AOT cache do Leyden via JEP 483 + JEP 515. Isso entrega ganho de cold start sem perder JIT em runtime.
- Endpoints frios e jobs efêmeros: mesmo código, build native via Quarkus + GraalVM Native Image, deploy separado como função. PGO opcional se a equipe estiver em Oracle GraalVM.
- GC: ZGC se tail latency é crítico (e agora compatível com AOT cache via JEP 516); G1 se throughput médio é o critério.
- Training run: parte do pipeline de CI/CD, com workload de produção replicado e versionado junto com o código.
Isso é cloud-native sem ruído: cada workload na ferramenta certa, sem ter que escolher um único caminho pra todo o sistema.
Conclusão
A decisão entre GraalVM Native Image e Project Leyden deixou de ser ideológica. Os dois caminhos amadureceram, cobriram seus principais gaps, e cada um tem cenário ideal. Quem ainda está nessa decisão sob a ótica de “qual é o futuro do Java” errou o foco: ambos são o futuro, em workloads diferentes.
O critério é arquitetural, não de hype. Conheça bem o seu workload, escolha a ferramenta certa, e invista no training run com o mesmo cuidado que você investe no código.
Se você está nessa decisão agora ou já passou por ela, me conta nos comentários: qual workload te empurrou pra um lado ou pro outro?