Compact Object Headers no JDK 27 reduz 22% do heap da JVM - Meu Universo Nerd

Existe um jeito de rodar a mesma aplicação Java usando bem menos memória, sem refatorar serviço nenhum. A maioria dos devs ainda não ligou essa configuração porque ela era opt-in, escondida atrás de uma flag experimental. No JDK 27 ela vira padrão, e quem entende o porquê sai na frente.

O recurso se chama Compact Object Headers (JEP 534) e, em benchmark oficial, corta cerca de 22% do heap e 8% de CPU. Bora ver, na prática, o que muda no cabeçalho de cada objeto que a sua JVM cria, como medir esse ganho no seu próprio código e em quais cenários ele realmente compensa.

Todo objeto Java carrega um cabeçalho que você nunca viu

Quando você faz new Pedido(), a JVM não guarda só os campos do seu objeto. Antes deles, ela coloca um cabeçalho (object header) com informações que a própria máquina virtual usa para funcionar: a mark word (usada por garbage collector, identity hash code e locks) e um ponteiro para a classe do objeto (o klass pointer, que diz "isto aqui é um Pedido").

Pense numa transportadora. Cada caixa que sai do galpão leva uma etiqueta com código de rastreio, peso e destino. Para uma geladeira, a etiqueta é irrelevante perto do produto. Agora imagine que você despacha milhões de parafusos, um por caixa. De repente as etiquetas pesam mais que a carga. É exatamente isso que acontece com a JVM em aplicações que criam milhões de objetos pequenos: o cabeçalho vira o protagonista do consumo de memória.

Na HotSpot de 64 bits, com compressed oops ligado (o padrão), o cabeçalho tradicional ocupa 12 bytes: 8 bytes da mark word e 4 bytes do klass pointer. Como a JVM alinha objetos em múltiplos de 8 bytes, um objeto pequeno costuma ser arredondado para 16 bytes. Para confirmar isso no seu próprio ambiente, use a biblioteca JOL (Java Object Layout), mantida pelo time da OpenJDK:

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.17</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;

public class HeaderDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

Rodando esse trecho num JDK 21 ou 25 sem nenhuma flag especial, a saída mostra o cabeçalho padrão ocupando os primeiros 12 bytes antes de qualquer campo:

java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION
     0     8        (object header: mark)     0x...
     8     4        (object header: class)    0x...
    12     4        (object alignment gap)
Instance size: 16 bytes

Repare nos últimos dois números: 4 bytes de "alignment gap" desperdiçados e 16 bytes de tamanho total para um objeto que, em tese, não guarda nada. Esse é o ponto cego de quem dimensiona memória olhando só para os campos. A fonte primária dessa mudança está documentada no JEP 534 (Compact Object Headers by Default), da OpenJDK.

O que o JDK 27 muda: de 12 para 8 bytes por objeto

O Compact Object Headers reescreve o layout do cabeçalho para caber em 8 bytes (64 bits). A ideia central é codificar a referência da classe dentro da própria mark word, em vez de manter um ponteiro separado. O resultado: o cabeçalho encolhe de 12 para 8 bytes, e muitos objetos pequenos passam a caber em 8 ou 16 bytes com bem menos desperdício de alinhamento.

Isso não nasceu no JDK 27. O recurso chegou como experimental no JDK 24 e ganhou maturidade como opt-in no JDK 25 (JEP 519). Lá, para ligar, você precisava destravar a opção experimental na linha de comando:

# JDK 24 e 25: precisava destravar a flag experimental
java -XX:+UnlockExperimentalVMOptions \
     -XX:+UseCompactObjectHeaders \
     -jar app.jar

# JDK 27: ja vem ligado por padrao, voce nao precisa de nada
java -jar app.jar

# Se precisar voltar ao comportamento antigo no JDK 27:
java -XX:-UseCompactObjectHeaders -jar app.jar

Com o JDK 27 em rampdown e GA marcado para 14 de setembro de 2026, essa flag deixa de ser um detalhe de especialista e passa a valer para todo mundo que só fizer o upgrade. É por isso que entender o mecanismo agora, antes de o GA chegar, é a jogada inteligente. Veja como o mesmo HeaderDemo se comporta com o recurso ativo:

java.lang.Object object internals (Compact Object Headers ON):
OFFSET  SIZE   TYPE DESCRIPTION
     0     8        (object header: mark+class)  0x...
Instance size: 8 bytes

O cabeçalho de 12 bytes virou 8, o gap de alinhamento sumiu e o objeto inteiro caiu de 16 para 8 bytes. Metade da memória, no caso extremo. Agora pense num objeto de domínio real, com dois inteiros, que é comuníssimo em chaves de cache, nós de grafo e DTOs internos:

public class Ponto {
    private final int x;
    private final int y;

    public Ponto(int x, int y) {
        this.x = x;
        this.y = y;
    }
    // 8 bytes de campos (dois int). Com cabecalho de 12 + alinhamento
    // = 24 bytes hoje. Com Compact Object Headers = 16 bytes.
}

São 8 bytes economizados por instância. Parece pouco até você multiplicar. Em um cache com 10 milhões desses pontos, a conta é direta: 10.000.000 vezes 8 bytes dá 80 MB de heap economizados, sem você tocar em uma única linha de lógica de negócio. Em estruturas de dados imutáveis e caches grandes, esse padrão se repete o tempo todo.

Por que 22% e não só "uns bytinhos"

O número de 22% não é chute de marketing. Ele vem da medição com o SPECjbb2015, o benchmark padrão de throughput da indústria para a JVM, citado no próprio JEP. A redução de heap fica em torno de 22% e, como há menos memória para alocar, varrer e mover, a CPU gasta cerca de 8% a menos no agregado. Menos pressão de memória significa menos pausas de garbage collector e mais ciclo de processador sobrando para o seu código.

E não é teoria de laboratório. A Amazon relatou rodar centenas de serviços em produção com Compact Object Headers ligado, e a SAP já ativou o recurso por padrão no SapMachine, a distribuição OpenJDK deles. Quando dois dos maiores operadores de JVM do planeta colocam algo em produção em escala antes de virar default, é sinal forte de que o recurso amadureceu.

Como Tech Leader, já vi um time inteiro abrir guerra contra um OutOfMemoryError intermitente num serviço que só fazia uma coisa: manter um cache enorme de objetos minúsculos. A reação automática foi pedir mais memória para o pod. O gargalo real, a gente descobriu depois, era justamente o overhead de cabeçalho que ninguém estava contabilizando. Na época, a saída foi reprojetar a estrutura para arrays primitivos. Hoje, boa parte daquele ganho viria de graça só com o upgrade de JDK.

O detalhe sênior: o que acontece com hashCode e locks

Aqui mora a parte que diferencia quem só leu o título de quem entende a engenharia por trás. A mark word de 64 bits sempre foi um espaço disputado: ela guarda bits de GC, o identity hash code (aquele valor que o Object.hashCode() padrão devolve) e o estado de lock do objeto. Ao enfiar a referência da classe dentro dessa mesma palavra, sobra menos espaço para o resto.

O time da OpenJDK resolveu isso reorganizando como esses dados convivem, sem mudar a semântica que o seu código enxerga. O identity hash code continua estável e o synchronized continua funcionando igual. O ponto de atenção fica para quem usa locking pesado: o mecanismo de displaced mark word, usado quando um objeto é travado, passou por ajustes para acomodar o novo layout. Para a esmagadora maioria das aplicações isso é invisível. Mas se a sua stack faz contenção alta de lock em objetos minúsculos, esse é o tipo de coisa que vale medir em homologação, não assumir.

Em outras palavras: a feature é transparente para o seu código, e mesmo assim conhecer esse encaixe entre mark word, hashCode e lock é o que mostra maturidade técnica de verdade. É a diferença entre "ativei a flag" e "sei o que a flag faz com a memória".

Quando vale, quando não muda nada e o que testar antes

O ganho de Compact Object Headers é proporcional a quantos objetos pequenos a sua aplicação mantém vivos ao mesmo tempo. Vale a pena prestar atenção nestes cenários:

  • Caches em memória com milhões de entradas pequenas (chaves, ids, pares coordenada)
  • Estruturas de dados com muitos nós: árvores, grafos, listas encadeadas, tries
  • Serviços com heap grande onde densidade de objetos define o custo de RAM por pod no Kubernetes
  • Aplicações que processam coleções enormes de DTOs ou eventos pequenos

Agora, onde você não deve esperar milagre:

  • Apps com poucos objetos grandes (o cabeçalho é irrelevante perto de um array de 1 MB)
  • Workloads CPU-bound que quase não alocam no heap
  • Código que depende de layouts de memória muito específicos via Unsafe ou bibliotecas nativas, onde vale testar com cuidado

O caminho seguro de adoção é simples. Antes do GA, pegue um serviço representativo, rode com -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders num JDK 25 em homologação e compare as métricas de heap e GC com e sem a flag. Você valida o ganho no seu próprio tráfego sem aposta no escuro. Se quiser revisar antes como ficou o panorama de threads e GC nas versões recentes, vale relembrar o que mudou nas Virtual Threads do Java 25 e como o JDK 27 trata o garbage collector G1 por padrão, porque os três assuntos se cruzam na hora de planejar a migração.

A pergunta de entrevista que essa feature responde

Em entrevista para vaga sênior, uma das perguntas que separam quem decorou framework de quem entende a plataforma é mais ou menos assim: "como você reduziria o custo de infraestrutura de uma stack Java sem reescrever o sistema?". Quem responde "subiria a memória do pod" mostra que trata o sintoma. Quem responde "olharia densidade de objetos e ativaria Compact Object Headers, que no JDK 27 já é padrão e economiza cerca de 22% de heap no SPECjbb2015" mostra que entende a JVM por dentro, conhece o roadmap e pensa em FinOps.

Essa é a diferença que pega bem numa conversa técnica: você não só sabe que existe, você sabe o número, sabe medir com JOL e sabe em qual cenário compensa. No mercado de hoje, esse tipo de resposta concreta vale mais que dez buzzwords.

Takeaways e próximo passo

  • Compact Object Headers encolhe o cabeçalho de 12 para 8 bytes e, no JDK 27, vem ligado por padrão
  • O ganho chega a 22% de heap e 8% de CPU no SPECjbb2015, maior em apps com muitos objetos pequenos
  • Você mede o impacto real com JOL e valida em homologação antes do GA de 14 de setembro de 2026, sem mudar uma linha de negócio

Você já testou Compact Object Headers na sua stack? Sua empresa está planejando o upgrade para o JDK 27? Conta nos comentários qual é o tamanho de heap que vocês carregam hoje, quero comparar os cenários com a galera.

Na próxima semana eu trago a contraparte deste assunto: como o G1 virar o coletor padrão até em container de 1 CPU no JDK 27 muda a sua latência, e o que testar para não tomar susto em produção.

Perguntas frequentes (FAQ)

Preciso recompilar ou mudar meu código para usar Compact Object Headers?
Não. O recurso atua no layout interno de memória da JVM, não na sua API. Você só faz o upgrade para o JDK 27, onde ele já vem ligado, ou ativa a flag em versões anteriores. O bytecode e o código-fonte continuam idênticos.

Funciona com compressed oops e com qualquer garbage collector?
Sim, ele opera junto com compressed oops e é suportado pelos coletores modernos da HotSpot, incluindo G1, Parallel e ZGC. O ganho específico varia conforme o perfil de alocação da aplicação, então meça no seu caso.

Como eu meço o ganho real na minha aplicação?
Use a biblioteca JOL para inspecionar o tamanho dos objetos com ClassLayout.parseInstance(obj) e compare o heap em homologação com a flag ligada e desligada. Olhe também as métricas de pausa de GC, porque menos memória costuma reduzir a frequência das coletas.

Tem algum risco em ligar isso em produção?
O recurso já roda em escala na Amazon e por padrão no SapMachine, então é maduro. O cuidado vale para código que depende de offsets de memória via Unsafe ou bibliotecas nativas. A recomendação é a de sempre: validar em homologação com o seu tráfego antes de promover.

Compact Object Headers substitui a otimização com tipos primitivos e records?
Não substitui, soma. Records e arrays de primitivos continuam sendo ótimas formas de reduzir alocação. Compact Object Headers é um ganho de base que se aplica a todos os objetos, em cima do que você já fizer de bom no design.