DEV Community

b3o
b3o

Posted on AI-assisted

Valhala: desmistificando uma das features mais aguardadas

Faz um tempo que eu fui fuçar no Project Valhalla e acabei ficando preso.

A proposta parece simples: e se nem todo dado pequeno precisasse virar um objeto completo no heap, com identidade, header e referência?

O projeto roda no OpenJDK desde 2014. Hoje ainda é experimental, mas já dá pra testar em builds early-access e acompanhar o que deve chegar nas próximas versões do Java. O ecossistema fala bastante do Java 26 em diante, e o Valhalla é um dos motivos.

O problema que demorei pra enxergar

Java divide o mundo em dois: primitivos (int, long, boolean...) e objetos (Integer, String, suas classes de domínio...).

Primitivo guarda valor direto na memória. Objeto vive no heap, é acessado por referência e passa pelo GC.

Isso funcionou por décadas. Só que no dia a dia a gente usa muita coisa pequena que na prática é só valor: coordenada, ID, intervalo de data, par de campos. Muitas vezes nem precisa de identidade. Só importa o conteúdo.

Identidade vs valor

Identidade é quando o objeto é único porque existe em um endereço próprio na memória:

String a = new String("b3o");
String b = new String("b3o");

System.out.println(a.equals(b)); // true
System.out.println(a == b);      // false
Enter fullscreen mode Exit fullscreen mode

equals compara conteúdo. == compara identidade.

Pra muita regra de negócio, um ponto (3, 4) é só (3, 4). Não importa "qual instância" é.

O custo invisível do modelo atual

Olha uma classe simples:

class Coordenada {
    int x;
    int y;
}
Enter fullscreen mode Exit fullscreen mode

Na cabeça são 8 bytes. Na prática entram object header, alinhamento, padding e referência. Esse objeto pode facilmente ocupar 16 ou 24 bytes.

Multiplica isso:

Coordenada[] coordenadas = new Coordenada[1_000_000];
Enter fullscreen mode Exit fullscreen mode

Um milhão de objetos. Um milhão de alocações. Um milhão de headers. Cache locality vai embora, memória aumenta e o GC trabalha mais.

Hoje o layout é mais ou menos assim:

[ref] -> { x, y }
[ref] -> { x, y }
[ref] -> { x, y }
Enter fullscreen mode Exit fullscreen mode

O Valhalla quer aproximar disso:

[x][y][x][y][x][y]
Enter fullscreen mode Exit fullscreen mode

Dados contíguos, menos pointer chasing, menos cache miss. Parecido com struct em C ou value type em C#, mas sem abandonar GC e tipagem do Java.

Boxing e generics

Outra dor clássica: generics não aceitam primitivo.

Não existe List<int>. Só List<Integer>.

List<Integer> numeros = List.of(1, 2, 3, 4, 5);
Enter fullscreen mode Exit fullscreen mode

Compila e roda. Só que cada Integer é um objeto com header e identidade. Em coleção grande, o custo some no code review e aparece no profiler.

O que o Valhalla quer resolver

Em linhas gerais:

  • tipos sem identidade, comparados por valor
  • armazenamento inline na memória (flattening)
  • menos alocação desnecessária
  • generics especializados sem boxing
  • menos pressão no GC

A pergunta que resume tudo: eu preciso saber qual instância é, ou só o valor importa?

Primitive classes na prática

Na sintaxe experimental (em builds recentes também aparece como value class), você declara tipos que se comportam como valor:

primitive class Point {
    int x;
    int y;
}
Enter fullscreen mode Exit fullscreen mode

Sem identidade. Imutabilidade é favorecida. A JVM pode guardar os campos inline, sem criar um objeto separado no heap para cada instância.

Composição e campos nullable

O interessante é que isso não fica limitado a tipos "planos". Dá pra compor:

primitive class Point {
    int x;
    int y;
}

primitive class Pessoa {
    Point location;
    float height;
    int[] childrenIds;
    String name;
}
Enter fullscreen mode Exit fullscreen mode

Aqui entram detalhes importantes:

  • location é outro value type. Em cenários favoráveis, a JVM pode achatar (flatten) os campos de Point dentro de Pessoa, em vez de ter referência para outro objeto.
  • height é primitivo. Também pode ficar inline.
  • childrenIds é array. Continua sendo referência, mas o array em si pode ser mais compacto dependendo do layout.
  • name é String, tipo de referência nullable. Campos nullable complicam flattening, porque null quebra layout inline previsível.

Ou seja: nem tudo vira bloco contínuo mágico. Tipos de referência e null ainda têm custo. Mas o modelo já permite expressar melhor o que é "só dado" e o que precisa de identidade.

Arrays achatados

Hoje, Point[] seria um array de referências. Cada posição aponta para outro lugar na memória.

Com Valhalla, um array de value type pode se comportar como bloco de valores:

[x][y][x][y][x][y]
Enter fullscreen mode Exit fullscreen mode

Isso muda o perfil de memória em matrizes, simulações, processamento geométrico e qualquer fluxo com milhões de registros pequenos.

L-World, Q-types e L-types (visão geral)

Sem entrar no nível de especificação da JVM, o modelo interno separa duas "projeções":

Conceito Ideia
Q-type Value type não-null, flattenable, sem identidade
L-type Referência tradicional, nullable, compatível com o Java de hoje

Resumindo: Q é valor puro inline. L é o wrapper/referência que o código legado já conhece.

O L-World é o esforço de unificar primitivos, objetos e value types sem quebrar bytecode e APIs antigas. A ideia não é substituir o Java atual. É fazer os dois mundos conviverem.

Null-restricted types

Outra peça do quebra-cabeça: deixar explícito se um tipo aceita null.

Isso ajuda a JVM a otimizar flattening e melhora segurança. null em campo inline é problema de layout e fonte de NullPointerException escondido.

No exemplo da Pessoa, String name aceita null. Já Point location em contextos null-restricted poderia ser tratado de forma mais agressiva pela JVM.

Specialized Generics

Hoje, List<T> com tipo primitivo ou value type costuma passar por boxing.

Com generics especializados, a ideia é gerar layouts específicos em tempo de compilação. Algo como List<Point> poderia armazenar valores inline, sem empilhar Integer-like wrappers.

Esse é um dos pontos com maior impacto prático, mas também um dos que mais demoram pra amadurecer no ecossistema inteiro (JDK, bibliotecas, frameworks).

Object header e Escape Analysis

Objetos Java carregam header com metadados de runtime (marcas de GC, identidade, etc.). Value types querem reduzir ou eliminar esse custo quando identidade não importa.

A JVM já faz Escape Analysis: o JIT tenta perceber que um objeto não "escapou" do método e pode eliminar a alocação, usando registradores. O problema é que isso não é garantido. Depende do código, do compilador e do caminho de execução.

O Valhalla quer tornar parte dessa otimização estrutural. Não depender só de "talvez o JIT resolva".

Impacto no GC

Menos objetos na heap significa:

  • menos tracing
  • menos scanning
  • menos compactação
  • menos pausas

GC moderno sofre com bilhões de micro-objetos. Valhalla ataca exatamente o padrão que a gente cria sem perceber em código "simples".

Onde estamos hoje

Ainda é experimental. Build separada, APIs em evolução, sintaxe em preview.

Nas builds mais recentes, a direção do OpenJDK aponta para value classes (JEP 401) como feature preview nas próximas versões da plataforma. A sintaxe primitive class que aparece em materiais e early-access pode variar conforme a build. Vale sempre olhar a documentação da build que você baixou.

Não é algo pra colocar em produção amanhã. Mas o problema que resolve é real e antigo.

Java ganhou o mundo pela produtividade. O Valhalla pergunta se dá pra manter isso sem pagar caro em todo valor pequeno.

Build pra testar:

https://jdk.java.net/valhalla/

Se você mexe com Java e já olhou pro profiler pensando "por que isso alocou tanto?", vale acompanhar. Não precisa virar especialista em JVM. Só entender o rumo já muda como você enxerga modelagem de dados no código.

Top comments (0)