Conexão · Interrompida

Algo não carregou

Parte desta página não chegou até você. Recarregue para tentar novamente — se persistir, verifique sua conexão.

Pular para o conteúdo principal
Engineering10 min de leitura

Java 26 Chegou: Os Recursos para Acompanhar Antes do LTS do Java 29

A maioria das equipes de backend roda apenas as linhas LTS, então o Java 26 parece dispensável. Não é — é o preview mais claro do que o LTS do Java 29 vai conter. Estas são minhas anotações sobre o que já está finalizado (cache de objetos AOT com ZGC, um G1 mais rápido, HTTP/3), o único preview pelo qual eu reescreveria código (Lazy Constants, com um exemplo executável) e a mudança de integridade que vai quebrar builds.

Todos os Posts
2/4

O Java 26 chegou à disponibilidade geral em 17 de março de 2026 com dez JEPs (página do projeto JDK 26). Se você faz deploy nas feature releases semestrais, já conhece o ritual. Se, como a maioria das casas de backend perto das quais trabalhei, você só roda as linhas LTS, é tentador pular esta e esperar a próxima release de longo prazo.

Esse é o instinto errado, e eis o porquê. O próximo LTS é o Java 29, previsto para setembro de 2027. Entre agora e lá estão exatamente três feature releases: 26, 27 e 28. Nada chega finalizado num LTS sem já ter amadurecido ao longo dessas releases intermediárias. Então o Java 26 não é uma release que você precise adotar. É o preview honesto mais antigo do que sua baseline do Java 29 vai conter. Lê-la agora é como você evita ser surpreendido por uma API finalizada — ou um build quebrado — daqui a dezoito meses.

Li todas as dez JEPs contra as fontes primárias. Abaixo está o recorte do engenheiro de backend: o que já está finalizado e, portanto, garantido para o 29, o único preview que mudou como escrevo código, os previews que vale acompanhar e a mudança com maior probabilidade de quebrar seu build.

Três recursos que já estão finalizados

Um recurso finalizado no 26 é um recurso com o qual você pode contar no 29. Três deles importam para o trabalho server-side.

O cache de objetos AOT agora funciona com qualquer garbage collector (JEP 516). Este é o trabalho de startup do Project Leyden, e a manchete é que o cache ahead-of-time não está mais atrelado a um GC específico. Antes do 26, o formato de objeto em cache era mapeado na memória num layout específico do GC, o que excluía o ZGC — então você tinha de escolher entre as pausas sub-milissegundo do ZGC e o startup mais rápido do Leyden (JEP 516). O Java 26 armazena objetos em cache num formato agnóstico de GC, como índices lógicos em vez de referências diretas, e uma thread em segundo plano os transmite para o heap no startup. Você opta por isso com -XX:+AOTStreamableObjects. Para qualquer coisa que inicia com frequência — funções serverless, pods com autoscaling, workers de CI — isso remove a razão pela qual os usuários de ZGC tinham de pular o cache AOT. Essa combinação é a que espero que se torne uma recomendação padrão quando o 29 for lançado.

O G1 ficou mensuravelmente mais rápido fazendo menos sincronização (JEP 522). Este é o ganho mais concreto da release. O G1 introduziu uma segunda card table para que as threads da aplicação sempre escrevam na tabela "ativa" sem coordenar com as threads de refinamento em segundo plano do G1. O resultado: a sincronização de memória entre o coletor e as threads da aplicação cai para aproximadamente o nível do Serial e do Parallel GC. Os números da JEP valem a citação — cargas de trabalho com muitas referências veem ganhos de throughput de 5–15%, as mais leves em torno de 5%, e no x64 a write barrier encolhe de cerca de 50 instruções para cerca de 12 (JEP 522). Não é de graça: cada card table custa 0,2% da capacidade do heap, cerca de 2 MB de memória nativa por 1 GB de heap. Para um serviço que gasta CPU de verdade na write barrier, esse é um preço irrisório por um aumento de throughput de dois dígitos, e o G1 ainda é o coletor padrão — então a maioria dos serviços herda isso sem mudar uma flag.

O HTTP/3 chegou ao HTTP Client (JEP 517). O cliente java.net.http agora sabe falar HTTP/3 sobre QUIC, sem exigir biblioteca de terceiros (JEP 517). Se você faz chamadas de saída para serviços ou gateways que suportam HTTP/3, você ganha, do cliente padrão, a prevenção de head-of-line blocking na camada de transporte. É uma mudança de configuração de requisição, não uma reescrita. Eu não migraria nada logo no primeiro dia, mas até o 29 este é o cliente HTTP de referência, e saber que ele existe muda como você avalia um upstream lento.

Lazy Constants: o preview pelo qual eu reescreveria código

O recurso que mudou como penso sobre inicialização é o Lazy Constants (JEP 526), um segundo preview no 26. Ele substitui um padrão que todo engenheiro de JVM já escreveu mal pelo menos uma vez: o singleton inicializado preguiçosamente e thread-safe, normalmente via double-checked locking com um campo volatile e um bloco synchronized.

LazyConstant<T> é um contêiner que computa seu valor no máximo uma vez, no primeiro acesso, por meio de um Supplier, e então o mantém como um valor efetivamente final. Como o valor se assenta num campo @Stable, o JIT pode fazer constant-folding dele após a inicialização — então, ao contrário de um holder feito à mão, você paga o imposto da inicialização adiada exatamente uma vez e as leituras seguintes podem ser tão baratas quanto ler uma constante de verdade (JEP 526).

Aqui está o padrão inteiro em um único arquivo executável. Trinta e duas virtual threads disputam a leitura do valor antes de ele existir; a ideia é que o supplier roda exatamente uma vez.

java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class LazyDemo {

    // Um campo substitui toda a dança do singleton com double-checked locking.
    private static final LazyConstant<Config> CONFIG = LazyConstant.of(LazyDemo::loadConfig);

    static final AtomicInteger supplierRuns = new AtomicInteger();

    record Config(int connections, String region) {}

    // Um inicializador caro para rodar no máximo uma vez, no primeiro uso.
    static Config loadConfig() {
        supplierRuns.incrementAndGet();
        try {
            Thread.sleep(50); // no lugar de ler um arquivo ou abrir um pool
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new Config(64, "eu-west-1");
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("before first get: initialized=" + CONFIG.isInitialized());

        // 32 threads disputam a leitura da constante antes de ela ser inicializada.
        var racers = IntStream.range(0, 32)
                .mapToObj(i -> Thread.ofVirtual().unstarted(() -> {
                    Config c = CONFIG.get(); // o primeiro chamador inicializa; o resto reutiliza
                    if (c.connections() != 64) {
                        throw new AssertionError("torn read");
                    }
                }))
                .toList();

        racers.forEach(Thread::start);
        for (Thread t : racers) {
            t.join();
        }

        System.out.println("after 32 racers: initialized=" + CONFIG.isInitialized());
        System.out.println("supplier ran " + supplierRuns.get() + " time(s)");
        System.out.println("region=" + CONFIG.get().region());
    }
}

Lazy Constants é uma API em preview, então compile e rode com a flag de preview:

javac --release 26 --enable-preview LazyDemo.java java --enable-preview LazyDemo

No JDK 26.0.1 isso imprime initialized=false, depois, após a disputa, initialized=true, supplier ran 1 time(s) e region=eu-west-1. Essa única linha — o supplier rodou uma vez apesar de 32 primeiros-leitores concorrentes — é toda a garantia de correção que você costumava escrever à mão e torcer para ter acertado.

Duas linhas merecem atenção. LazyConstant.of(LazyDemo::loadConfig) vincula o supplier sem executá-lo; nada é computado até o primeiro get(). E CONFIG.isInitialized() permite verificar o estado sem forçar a inicialização, o que é útil para health checks que não deveriam disparar justamente o trabalho que estão sondando. A API também cobre coleções: List.ofLazy(size, indexFn) e Map.ofLazy(keys, valueFn) dão a você estruturas preguiçosas elemento a elemento, em que cada slot inicializa de forma independente no primeiro toque — uma tabela de lookup memoizada sem um mapa concorrente e seu overhead por leitura.

A ressalva é o próprio status de preview. A superfície da API pode mudar antes de se finalizar, e eu ainda não a espalharia por uma base de código grande. Eu a mantenho num branch descartável para sentir a ergonomia. Essa cautela já se justifica: a JEP 531 desde então mirou um terceiro preview para o Java 27 que remove isInitialized() e orElse e adiciona Set.ofLazy — então a chamada a isInitialized() acima ainda compila no 26, mas já é um dos métodos a caminho da saída (JEP 531). Quando o 29 chegar, é ainda assim que vou escrever inicialização preguiçosa — e o double-checked locking passa a ser um code smell em vez de um mal necessário.

Os previews que vale acompanhar, mas ainda não adotar

Mais dois previews moldam como será a sensação do 29.

Tipos primitivos em patterns, instanceof e switch chegou ao seu quarto preview (JEP 530). Ele permite que o pattern matching funcione diretamente sobre primitivos — switch sobre um int com patterns em estilo de intervalo, instanceof estreitando para um primitivo — fechando a lacuna que fazia o pattern matching parecer um recurso só para tipos de referência. Já no quarto preview, está perto de pronto, e é o tipo de mudança de linguagem que silenciosamente remove uma categoria de boilerplate quando está em todo lugar.

A structured concurrency chegou ao seu sexto preview (JEP 525). Tem sido o mais lento dos recursos modernos de concorrência a se assentar, e a API mudou bastante ao longo dos previews. Escrevi em um post separado como suas semânticas de falha e cancelamento se comparam entre runtimes, então não vou reexplicá-la aqui — o ponto relevante para planejamento é que seis previews sinalizam uma API ainda encontrando sua forma final, então eu trataria qualquer coisa que você construa sobre ela hoje como provisória até que se finalize, plausivelmente no 29.

A Vector API permanece em incubação, sua décima primeira rodada (JEP 529). Vem incubando há anos à espera do Project Valhalla, e não há finalização no horizonte. Acompanhe-a se você faz trabalho numérico ou pesado em SIMD; não faça planos contando com a chegada dela no 29.

O que vai quebrar: final começa a significar final

A mudança com maior probabilidade de mexer no seu build não é nem um recurso. "Prepare to Make Final Mean Final" (JEP 500) é o trabalho de integridade por padrão: o JDK agora emite um aviso quando o código usa deep reflection para mutar campos declarados final (recursos do JDK 26). Por anos, bibliotecas de serialização, frameworks de mock e contêineres de injeção de dependência penetraram e reescreveram campos final. O Java 26 avisa sobre isso; uma release futura vai negar por padrão. Se sua suíte de testes ou seus logs de startup se acenderem com avisos sobre mutação de campo final, isso é uma dependência fazendo algo que vai parar de funcionar — melhor descobrir qual agora do que durante uma migração para o Java 29 sob prazo.

A outra remoção é menor: a Applet API se foi (JEP 504). Está depreciada há anos e inutilizável em navegadores modernos há mais tempo ainda. Se algo que você mantém ainda importa java.applet, o 26 é onde ele para de compilar. Menciono isso apenas porque "removido" é o único status que transforma um aviso em uma falha.

O que fazer agora se você vive no LTS

Você não precisa rodar o Java 26 em produção para se beneficiar dele. Você precisa lê-lo como uma previsão.

  • Experimente o cache de objetos AOT com ZGC (-XX:+AOTStreamableObjects) em um serviço sensível a startup e anote os números — essa combinação é forte candidata a se tornar uma recomendação padrão até o 29.
  • Espere o ganho de throughput do G1 de graça quando você eventualmente migrar para o 29; se você faz profiling de write barriers quentes hoje, esses 5–15% são folga real já garantida.
  • Prototipe Lazy Constants em um branch para aposentar um singleton com double-checked locking, mas mantenha-o fora do código compartilhado até o preview se finalizar.
  • Rode sua suíte de testes atual em um JDK 26 agora e faça grep nos logs por avisos de mutação de campo final. Cada aviso é uma dependência que você terá de atualizar ou substituir antes do 29.
  • Trate structured concurrency e primitive patterns como "vindo, não aqui" — vale a leitura, não vale apostar um design até que se finalizem.

O Java 26 é uma release de curto prazo com uma janela de suporte de seis meses, então a maioria das equipes nunca vai fazer deploy dele. Tudo bem. Seu valor real é como um ensaio: os recursos finalizados são o piso do seu Java 29, os previews são seu provável teto, e os avisos de integridade são sua lista de tarefas de migração, disponível um ano e meio antes.

Continue lendo

Curtindo? Talvez goste disso aqui.

Nada parecido — quer tentar outro ângulo?

Isso foi útil?

Deixe uma avaliação ou uma nota rápida — me ajuda a melhorar.