Imagina a cena: você só trocou a tag da imagem Docker de eclipse-temurin:26 para eclipse-temurin:27. Não mexeu em uma linha de código, não tocou em nenhuma flag da JVM, não mudou o requests.cpu do pod. Faz o deploy, vai tomar um café, e quando volta aquele microsserviço de 1 vCPU está gastando mais CPU e a latência sob carga começou a oscilar feio. O time de SRE abre um chamado, o time de dev jura que não mudou nada. E os dois estão certos.
O culpado tem nome: JEP 523, que chega no JDK 27 e faz o G1GC virar o coletor de lixo padrão em todos os ambientes, inclusive nos containers de 1 CPU que hoje rodam SerialGC sem você nem saber. O JDK 27 entrou em Rampdown Phase One em junho e tem GA marcada para 14 de setembro de 2026. Ou seja, tem uma janela curta pra você medir seus pods sub-dimensionados antes que esse default mude embaixo deles. Bora entender o que muda, como detectar e como fixar isso direito.
O que o JEP 523 muda (e o que acontecia antes)
Por muito tempo a JVM escolheu o Garbage Collector sozinha, usando uma regra de ergonomia baseada no hardware que ela enxerga. Se a máquina fosse "server-class", ou seja, com 2 ou mais processadores e pelo menos 2 GB de RAM, ela ligava o G1GC. Abaixo disso, caía no SerialGC, que é single-thread, tem overhead baixíssimo e funciona bem em heap pequeno.
O problema é que essa regra foi pensada para máquina física, não para container. No Kubernetes, é super comum limitar um pod a menos de 2 CPUs efetivas e menos de 2 GB. Resultado: um monte de aplicação Java rodava SerialGC em produção sem ninguém ter decidido isso de propósito. A escolha era silenciosa. O JEP 523 muda exatamente esse comportamento: a partir do JDK 27, o G1 é o padrão em todo ambiente, sem o fallback automático pra SerialGC em container pequeno.
Repara que é o oposto do que muita manchete diz por aí. Não é "agora pod pequeno vai pra SerialGC". É o contrário: pod pequeno que estava no SerialGC vai pro G1. Dá pra ver isso na prática com uma linha:
# JDK 26, container de 1 vCPU e 1Gi de RAM
$ java -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseSerialGC"
bool UseG1GC = false {product}
bool UseSerialGC = true {product} # SerialGC escolhido sozinho
# JDK 27, MESMO container, MESMAS flags
$ java -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseSerialGC"
bool UseG1GC = true {product} # agora G1 por padrao
bool UseSerialGC = false {product}
Mesma imagem base, mesmo limite de recurso, comportamento diferente. É esse tipo de mudança que não aparece no diff do seu Git e por isso pega o time desprevenido. Como Tech Leader, já vi uma migração de JDK travar uma madrugada inteira por causa de um default que ninguém sabia que existia. A lição que ficou: o que a JVM decide sozinha é justamente o que você precisa conhecer.
De onde vem essa regra: ergonomia, cgroups e o limite de memória
Pra entender por que o seu pod caía no SerialGC, precisa olhar como a JVM define "server-class". A regra clássica é direta: a máquina precisa ter pelo menos 2 processadores disponíveis e pelo menos 2 GB de RAM. Tem até um número de memória que vira a fronteira na prática. Até por volta de 1791 MB a ergonomia puxa pro SerialGC, acima disso liga o G1. Esses dois valores juntos, CPU e RAM, decidem o coletor antes mesmo da sua aplicação subir.
O detalhe que faz isso explodir em Kubernetes é o cgroup. Desde o JDK 10, com backport no 8u191, a JVM respeita os limites do container porque o UseContainerSupport vem ligado por padrão. Então quando você define resources.limits.cpu: "1" no manifesto, a JVM enxerga 1 processador, não os 64 do nó físico. E aí, com uma CPU visível, a ergonomia decide SerialGC. Dá pra ver exatamente o que a JVM enxerga:
public class Visiveis {
public static void main(String[] args) {
Runtime rt = Runtime.getRuntime();
System.out.println("CPUs visiveis: " + rt.availableProcessors());
System.out.println("Heap maximo (MB): " + (rt.maxMemory() / 1_048_576));
}
}
Roda isso dentro do pod, com o limite de produção. Se availableProcessors devolve 1 e o heap máximo fica abaixo de 1792 MB, é praticamente certo que você estava no SerialGC no JDK 26. E é exatamente esse cenário que o JEP 523 vira do avesso: a partir do 27, mesmo com uma CPU visível, o G1 entra. Bora ver como confirmar isso antes do upgrade.
Como saber qual GC seu pod usa hoje
Antes de migrar, o primeiro passo é saber o que você tem hoje rodando. O comando de PrintFlagsFinal ali em cima resolve no startup, mas dentro de uma aplicação que já está no ar dá pra checar em runtime, sem reiniciar nada. A JVM expõe isso pelo GarbageCollectorMXBean:
import java.lang.management.ManagementFactory;
import java.lang.management.GarbageCollectorMXBean;
public class QualGc {
public static void main(String[] args) {
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println("GC ativo: " + gc.getName());
}
// SerialGC -> imprime "Copy" e "MarkSweepCompact"
// G1GC -> imprime "G1 Young Generation" e "G1 Old Generation"
}
}
Se você está num ecossistema Spring Boot, dá pra ir além e expor isso como métrica. O Micrometer já publica jvm.gc.pause e jvm.gc.memory.allocated pelo Actuator, então você consegue comparar o número de pausas e o tempo total parado antes e depois do upgrade, sem chutar. Esse é o tipo de dado que transforma "achei que ficou mais lento" em "as pausas de GC subiram 30% nesse pod". Quer saber qual número olhar? Anota: frequência de coleta e p99 de pausa são os dois que mais contam num pod pequeno.
O jeito errado e o jeito certo de subir pro JDK 27
O jeito errado é o mais tentador, porque é o que a gente sempre fez: subir a versão e confiar que o default novo é melhor pra todo mundo. Funciona na maioria dos casos. Só que "maioria" não é "todos", e em pod minúsculo o G1 tem um custo estrutural (threads de coleta concorrentes, mais estruturas internas) que o SerialGC não tem.
O jeito certo é tirar a decisão do automático e fixar o GC de propósito, com base em medição. Em vez de deixar a ergonomia decidir, você declara a intenção no manifesto da imagem:
# Jeito ERRADO: confiar no default e descobrir em producao
FROM eclipse-temurin:27-jre
# (nenhuma flag de GC, a JVM escolhe sozinha)
# Jeito CERTO: fixar o GC de proposito
FROM eclipse-temurin:27-jre
# pod pequeno e I/O-bound que ia bem no Serial? mantenha explicito:
ENV JAVA_TOOL_OPTIONS="-XX:+UseSerialGC"
# vai adotar o G1 de proposito? entao defina a meta de pausa e meca:
# ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
Não é que SerialGC seja melhor que G1. É que a escolha precisa ser sua, documentada, e não um efeito colateral de qual número de CPU o pod calhou de ter. Fixar a flag também deixa seu comportamento de produção igual entre JDK 26 e 27, o que torna o upgrade um passo de cada vez em vez de duas mudanças ao mesmo tempo (versão nova + GC novo).
Bora pro detalhe que separa o time que mede do time que reza: como saber se o G1 te ajuda ou te atrapalha nesse pod específico.
O impacto real: quando o G1 ajuda e quando o Serial ainda vence
A intenção do JEP 523 é boa e tem base técnica de verdade. Desde o JDK 20, o time do G1 removeu um dos marking bitmaps, o que cortou bastante o consumo de memória nativa do coletor. Por isso ele ficou viável até em container pequeno, e em muitos casos onde antes caía SerialGC o G1 melhora a latência máxima, porque faz coleta concorrente em vez de parar o mundo inteiro.
O ponto cego é o pod realmente apertado, tipo 1 vCPU e heap de algumas centenas de MB, com carga de alocação alta. Ali o G1 paga overhead por um paralelismo que não tem CPU pra exercer. Você consegue reproduzir isso com um stress simples de alocação e comparar os logs de GC:
public class GcStress {
public static void main(String[] args) {
Random rng = new Random(42);
long fim = System.currentTimeMillis() + 30_000;
long totalBytes = 0;
while (System.currentTimeMillis() < fim) {
byte[] lixo = new byte[rng.nextInt(4_096) + 256]; // objetos pequenos
totalBytes += lixo.length;
}
System.out.println("Total alocado: " + (totalBytes / 1_048_576) + " MB");
}
}
Roda esse mesmo programa duas vezes, com java -Xlog:gc -XX:+UseSerialGC GcStress e depois com -XX:+UseG1GC, num container limitado a 1 CPU. Compara o número de coletas, o tempo total parado e o pico de CPU. Em workload I/O-bound com poucos objetos vivos, o Serial costuma gastar menos CPU; em heap maior com muita promoção pra old gen, o G1 ganha em pausa. O número é seu, não da manchete.
Na prática, a régua que uso é simples. Pod com 2 ou mais vCPUs e heap acima de 1 GB: deixa o G1, é o caminho que o JEP 523 oficializa e geralmente vence. Pod de 1 vCPU, heap pequeno e muito I/O: testa o Serial explícito antes de aceitar o novo default. E em qualquer caso, fixa a flag para o upgrade não virar duas variáveis mudando juntas.
Pra deixar concreto, um caso que peguei. Um serviço de borda em 1 vCPU e 512 MB de heap, com taxa alta de alocação de objetos curtos (parsing de JSON em cima de uma fila). No JDK 26 com SerialGC ele fazia coletas rápidas e baratas, p99 de pausa na casa de poucos milissegundos. Forçando o G1 no mesmo pod, o número de ciclos caiu, mas cada pausa ficou mais cara e o uso de CPU base subiu, porque o G1 mantém threads e estruturas que aquele 1 vCPU não tinha folga pra carregar. Já num serviço de catálogo com 4 vCPUs e 3 GB de heap, foi o inverso: o G1 cortou os picos de pausa quase pela metade. Mesmo binário, decisão oposta, e os dois certos.
| Cenário do pod | Melhor escolha | Por quê |
|---|---|---|
| 1 vCPU, heap pequeno, I/O-bound | SerialGC explícito | Overhead mínimo, sem CPU para paralelismo |
| 2+ vCPUs, heap acima de 1 GB | G1 (novo default) | Coleta concorrente, corta picos de pausa |
| Latência crítica, heap grande | Avaliar ZGC | Pausas sub-milissegundo, custo de memória maior |
E o ZGC, já que estamos no assunto? É a pergunta natural do sênior. O Generational ZGC amadureceu e entrega pausas sub-milissegundo, mas custa mais memória e não é o default. A régua continua a mesma: o JEP 523 te dá o G1 de graça como padrão razoável, e você sobe pro ZGC de propósito quando latência for requisito de negócio, não por hype. Tem um panorama bem feito dos coletores em 2026 no guia da foojay, vale a leitura antes de qualquer decisão de arquitetura.
Resumo e próximo passo
Recapitulando o que importa antes de você encostar no JDK 27 em produção:
- JEP 523 torna o G1 o GC padrão em todo ambiente, inclusive container de 1 CPU que hoje cai no SerialGC. A mudança é silenciosa e não aparece no seu código.
- Detecte antes de migrar: use
PrintFlagsFinalno startup e o GarbageCollectorMXBean (ou as métricas do Actuator) em runtime para saber o que roda hoje. - Fixe o GC de propósito em vez de confiar no automático, e meça pod a pod: G1 ganha em heap maior, Serial ainda vence em pod minúsculo e I/O-bound.
E é isso. O JDK 27 não está te sabotando, ele está mudando um padrão antigo que fazia sentido para servidor físico e fazia menos sentido para container. Quem mede sai na frente, quem confia no default cego descobre em produção.
Você já fixa o GC nos seus pods ou deixa a JVM escolher? Já tomou susto com mudança de default em upgrade de JDK? Conta nos comentários, quero ver como está sendo na stack de vocês. E se esse tema te interessa, dá uma olhada também no que o mesmo JDK 27 faz com a memória dos seus objetos no artigo sobre Compact Object Headers no JDK 27, que economiza heap sem você mudar uma linha.
Perguntas frequentes (FAQ)
O JEP 523 remove o SerialGC?
Não. O SerialGC continua no JDK 27 e pode ser ligado a qualquer momento com -XX:+UseSerialGC. O que muda é só o padrão automático, que passa a ser o G1 em todos os ambientes.
Preciso mudar meu código por causa disso?
Não. É uma mudança de runtime, não de API. Seu código Java e seu projeto Spring Boot continuam iguais. O que muda é o coletor que roda por baixo, e você controla isso por flag da JVM.
Como sei se meus pods estavam usando SerialGC sem eu saber?
Rode java -XX:+PrintFlagsFinal -version | grep UseSerialGC dentro de um container com o mesmo limite de CPU e RAM da produção, ou cheque o GarbageCollectorMXBean em runtime. Se aparecer "Copy" e "MarkSweepCompact", é SerialGC.
O G1 vai sempre deixar meu pod mais lento no JDK 27?
Não. Na maioria dos casos onde antes caía SerialGC, o G1 melhora a latência máxima, porque coleta de forma concorrente. O risco fica em pods muito apertados (1 vCPU, heap pequeno) com alta alocação, onde vale medir antes de aceitar o novo default.
Quando o JDK 27 fica disponível em GA?
A GA está marcada para 14 de setembro de 2026. O Rampdown Phase One começou em junho de 2026, então o conjunto de features, incluindo o JEP 523, já está congelado. Dá pra testar agora pelo build de early access. Veja o anúncio oficial no inside.java.
Devo pular o G1 e ir direto pro ZGC no JDK 27?
Só se latência for requisito de negócio e você tiver memória sobrando. O Generational ZGC entrega pausas sub-milissegundo, mas consome mais RAM e não é o default. Para a maioria dos serviços, o G1 que o JEP 523 oficializa é o ponto de partida certo. O ZGC é uma escolha deliberada, não automática.
Quer ir mais fundo na evolução recente da JVM? Confira também nossos artigos sobre Jackson 3 e Spring Boot 4 e sobre Spring AI na prática.