Project Valhalla no JDK 28: 197 Mil Linhas de Código Para Mudar o Java Para Sempre
Dez anos. Foi esse o tempo que a equipe do OpenJDK levou para entregar algo que, à primeira vista, parece simples: fazer o Java tratar objetos como se fossem primitivos. Por trás dessa frase inocente está o Project Valhalla — a maior cirurgia na JVM desde que o Java existe. E ele finalmente chegou ao JDK 28.
A JEP 401, que introduz value classes e value objects, foi integrada ao mainline do OpenJDK com um pull request de 197.000 linhas em 1.816 arquivos modificados. Pra colocar em perspectiva: isso é mais código do que muitos projetos inteiros no GitHub. E tudo isso pra resolver um problema que incomoda devs Java desde 1995.
O problema que existia desde o primeiro dia
Java sempre teve dois mundos separados: primitivos (int, double, boolean) e objetos (Integer, String, Point). Os primitivos são rápidos — ficam na stack, não passam pelo garbage collector, não têm overhead de memória. Objetos, por outro lado, vivem no heap, carregam um header de 12 a 16 bytes só pra existir, e precisam de um ponteiro pra cada referência.
Isso criou uma tensão permanente na linguagem. Quer performance? Use int[]. Quer abstração, encapsulamento e type safety? Use Integer. Mas nunca os dois ao mesmo tempo.
// Isso é rápido mas primitivo
int[] coords = new int[1_000_000];
// Isso é elegante mas lento
record Point(int x, int y) {}
Point[] points = new Point[1_000_000]; // 1 milhão de objetos no heap, cada um com header + ponteiro
Aquele array de Point ocupa pelo vezes mais memória do que o necessário. Cada Point tem seu header de objeto, seu ponteiro no array, e fica espalhado pelo heap em posições aleatórias — péssimo pra cache locality.
O Project Valhalla resolve exatamente isso.
Value classes: “Codifica como classe, roda como int”
O conceito central do Valhalla é o value class. Você declara uma classe com o modificador value, e a JVM passa a tratá-la de forma radicalmente diferente: sem identidade, sem header, sem overhead.
value class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Essa classe se comporta como qualquer outra na superfície — tem métodos, pode implementar interfaces, participa de hierarquias. Mas por baixo do capô, a JVM pode:
- Eliminar a alocação (scalarization): ao invés de criar um objeto
Pointno heap, o JIT decompõe a referência nos campos individuaisxey, passando-os diretamente nos registradores da CPU. - Achatar no heap (heap flattening): quando um
Pointprecisa existir em memória (num campo ou array), seus dados são escritos diretamente no local, sem ponteiro intermediário.
Na prática, um Point[] com value classes vira tão compacto quanto um int[] com o dobro de elementos. Sem headers, sem ponteiros, sem saltos aleatórios de memória.
Scalarization: o truque que mata alocações
A scalarization é onde o ganho de performance realmente aparece no dia a dia. Quando o JIT compiler encontra uma value class, ele faz algo que a escape analysis tradicional nunca conseguiu de forma confiável: quebra o objeto em pedaços e passa cada campo como valor independente.
value class Color {
byte r, g, b;
Color(byte r, byte g, byte b) {
this.r = r;
this.g = g;
this.b = b;
}
}
// Em código normal:
Color c = new Color((byte) 255, (byte) 128, (byte) 0);
processColor(c);
// O que o JIT faz por baixo:
// Nenhum objeto é alocado. processColor recebe 3 bytes nos registradores.
A diferença fundamental em relação à escape analysis é que a scalarization de value classes funciona entre boundaries de métodos, mesmo sem inline. A escape analysis clássica só otimiza objetos que não “escapam” do método que os criou — e quebra com frequência quando o método é grande demais pra inline.
Com value classes, a otimização é previsível e garantida por contrato. A JVM sabe que aquele objeto não tem identidade, então pode decompô-lo sempre.
Heap flattening: arrays densos de verdade
O segundo pilar de performance é o heap flattening. Quando um value object precisa existir em memória — armazenado num campo ou numa posição de array — a JVM escreve os dados diretamente no local, sem indireção.
// Array de objetos tradicionais na memória:
[ptr_0] → [header | x | y] (em algum lugar do heap)
[ptr_1] → [header | x | y] (em outro lugar do heap)
[ptr_2] → [header | x | y] (em mais outro lugar)
// Array de value objects com flattening:
[x|y|x|y|x|y] (tudo contíguo, sem headers, sem ponteiros)
Isso não é só uma economia de memória. É uma transformação completa no padrão de acesso à memória. CPUs modernas puxam dados em blocos de 64 bytes (cache lines). Com o layout tradicional, cada acesso a um Point diferente no array provavelmente causa um cache miss porque os objetos estão espalhados pelo heap. Com flattening, os dados ficam contíguos — e a CPU carrega dezenas de Points de uma vez.
Os benchmarks iniciais mostram ganhos de 3 a 4x em cenários com arrays grandes de objetos pequenos — exatamente o tipo de workload que domina processamento numérico, renderização, e pipelines de dados.
Tem uma restrição importante: 64 bits
Pra que o flattening funcione atomicamente — ou seja, sem que uma thread leia um valor parcialmente escrito por outra — o value object precisa caber em 64 bits (8 bytes), incluindo um bit de flag pra null.
Um Point com dois ints soma 64 bits. Perfeito, cabe. Mas um Color com quatro floats soma 128 bits — grande demais pra atomic flattening nos processadores atuais.
| Cenário | Bits necessários | Flattening atômico? | |
|---|---|---|---|
| ——— | —————– | ——————— | |
Point(int x, int y) |
64 | Sim | |
Color(byte r, byte g, byte b) |
24 + null bit | Sim | |
Money(long amount, Currency c) |
64 + referência | Não | |
Matrix3x3(float[9]) |
288 | Não |
Classes maiores ainda se beneficiam da scalarization, mas não recebem flattening no JDK 28. Futuras versões devem suportar encodings de 128 bits, e os null-restricted types (tipos não-nulos, marcados com !) vão permitir economizar o bit de null e empurrar mais classes pro limite atômico.
O que muda pra quem programa Java no dia a dia
O operador == agora compara conteúdo
Pra value classes, == testa substitutabilidade: dois value objects com os mesmos campos são considerados iguais. Não existe identidade — não existe “este é o mesmo objeto na memória”.
value class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a == b); // true — mesmos campos, mesmo valor
Em classes tradicionais, a == b seria false porque são objetos diferentes na memória. Essa mudança parece pequena, mas elimina uma categoria inteira de bugs onde devs esquecem de usar .equals().
Synchronized é proibido
Value classes não têm identidade, então não podem ser usadas como lock de synchronization. Tentar synchronized(valueObj) gera um IdentityException em runtime, ou um erro de compilação quando o compilador consegue detectar.
value class Token { String value; }
Token t = new Token("abc");
synchronized (t) { // IdentityException!
// ...
}
Isso é proposital. Se dois value objects com os mesmos campos são intercambiáveis, qual deles você está “travando”? O conceito não faz sentido.
Wrappers de primitivos viram value classes
Uma das migrações mais impactantes: as classes wrapper (Integer, Long, Double, etc.) são convertidas para value classes no modo preview. Isso significa que o boxing — aquela operação que todo dev Java aprende a evitar — fica drasticamente mais barato.
// Antes: cada Integer é um objeto separado no heap
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Com Valhalla (preview): Integer é value class
// O JIT pode scalarizar e achatar, reduzindo overhead de boxing
Integer[] se aproxima da eficiência de int[] em cenários onde o flattening se aplica. Isso é enorme pra qualquer código que usa generics com números.
O que ainda NÃO está no JDK 28
Valhalla é um projeto multi-release. O JDK 28 entrega a primeira preview, mas faltam peças importantes:
Null-restricted types
A possibilidade de marcar um tipo como não-nulo (Point!) foi adiada para uma JEP separada. Sem ela, value classes ainda precisam gastar um bit pra representar null, limitando o flattening de classes maiores.
Specialized generics
O sonho de ter ArrayList — generics sobre primitivos sem boxing — depende de outra fase do projeto. Com type erasure, o ArrayList atual ainda armazena referências, não valores achatados.
Encodings de 128 bits
Processadores x86 e ARM suportam operações atômicas de 128 bits, mas a JVM ainda não aproveita isso. Quando suportar, value classes maiores poderão receber flattening.
10 anos de design: por que demorou tanto?
Eu sei o que você está pensando: “C# tem struct desde 2002, Kotlin tem inline class desde 2019. Por que o Java levou uma década?”
A resposta curta: retrocompatibilidade. O Java não podia simplesmente adicionar um novo tipo de classe — precisava fazer isso de um jeito que:
- Binários existentes continuem funcionando sem recompilação
- APIs como
java.lang.Integerpossam migrar sem quebrar código de terceiros - O modelo de memória da JVM continue seguro em cenários concorrentes
- A JIT compilation funcione corretamente com as novas semânticas
C# criou struct do zero. O Java precisou abrir o peito de uma JVM de 30 anos e reconfigurar o motor com ele rodando.
Brian Goetz, o arquiteto por trás do Valhalla, resumiu o desafio numa frase: “Não estamos adicionando um recurso ao Java. Estamos removendo uma suposição que existia desde 1995 — que todo objeto tem identidade.”
Remover uma suposição que está embedada em cada otimização do JIT, cada layout de memória do GC, e cada instrução de bytecode… é isso que leva 197.000 linhas e uma década.
Como testar agora
O JDK 28 deve chegar em março de 2027, mas você já pode testar com early access builds:
# Baixar o EA build do JDK 28
# https://jdk.java.net/28/
# Compilar com preview habilitado
javac --enable-preview --source 28 ValhallaTest.java
# Executar com preview
java --enable-preview ValhallaTest
// ValhallaTest.java
value class Vec2 {
double x, y;
Vec2(double x, double y) {
this.x = x;
this.y = y;
}
Vec2 add(Vec2 other) {
return new Vec2(this.x + other.x, this.y + other.y);
}
@Override
public String toString() {
return "Vec2(" + x + ", " + y + ")";
}
}
void main() {
Vec2 a = new Vec2(1.0, 2.0);
Vec2 b = new Vec2(3.0, 4.0);
Vec2 c = a.add(b);
System.out.println(c); // Vec2(4.0, 6.0)
System.out.println(a == new Vec2(1.0, 2.0)); // true
}
Lembre que é preview feature: precisa da flag --enable-preview e o código não é garantido como estável entre releases.
Quem deveria se importar (e quem pode esperar)
Se você trabalha com:
- Processamento numérico (financial, scientific computing): o impacto é direto e massivo
- Game engines ou simulações em Java: arrays de vetores e matrizes com flattening mudam tudo
- Aplicações de alto throughput (trading, streaming): menos GC pressure = latência mais previsível
- Frameworks e libraries (Jackson, Spring, etc.): adaptar pra value classes pode beneficiar todos os usuários
Se você faz CRUD com Spring Boot e PostgreSQL… o JDK 25 LTS te atende bem por mais alguns anos. O próximo LTS com Valhalla estabilizado provavelmente será o JDK 29 (setembro de 2027).
Mas mesmo que você não use value classes diretamente, vai se beneficiar. As classes wrapper (Integer, Long, Double) virando value classes significa que qualquer código que usa boxing — e isso é quase todo código Java — fica mais rápido sem mudar uma linha.
O futuro que o Valhalla abre
O que mais me empolga no Valhalla não é o que ele entrega agora — é o que ele destranca. Com value classes como fundação, o caminho fica livre pra:
- Specialized generics:
Listsem boxing,Mapsem wrappers - Tipos não-nulos:
Point!que nunca é null, eliminando NullPointerExceptions no nível do tipo - Arrays flat de qualquer tamanho: com encodings maiores, até matrizes 3D podem ser contíguas
O Java sempre foi aquela linguagem que sacrificava performance por segurança e portabilidade. O Valhalla é a primeira vez que ele diz: “por que não os dois?”
Se C# tem struct e Kotlin tem inline class, agora o Java tem a versão que — tipicamente — levou mais tempo, mas tentou fazer certo desde o início. Daqui a dois anos, quando specialized generics chegarem e ArrayList funcionar nativamente, vamos olhar pra trás e perceber que o JDK 28 foi onde tudo começou.
Fonte de inspiração: Project Valhalla, Explained: How a Decade of Work Arrives in JDK 28 — JVM Weekly













