Tem uma flag que metade dos pipelines de CI carrega há anos sem ninguém lembrar por quê: -noverify. Alguém colocou lá em 2017 pra "acelerar o startup" pulando a verificação de bytecode, o build ficou verde, e ninguém mais tocou. Até o dia em que você troca a imagem base pra eclipse-temurin:27 e o container simplesmente não sobe. Nos logs, uma linha seca: Unrecognized option: -noverify.
O JDK 27 parou de ser tolerante com quatro opções antigas de launcher: -noverify, -Xverify:none, -noclassgc e -verifyremote. Elas estavam depreciadas (com aviso) desde o Java 13, e agora viraram erro fatal: a JVM nem inicia. Se alguma sobreviveu num Dockerfile, num argLine do Surefire ou numa run config de IDE, o build vai parar de subir bem na hora do upgrade. Bora ver onde isso se esconde, como varrer o projeto antes e como arrumar direito.
O que o JDK 27 removeu (e o que continua valendo)
Por anos o launcher do Java aceitou opções obsoletas só imprimindo um aviso e seguindo em frente. Isso acabou. No JDK 27, estas quatro foram removidas de vez:
-noverifye-Xverify:none: desligavam a verificação de bytecode no carregamento das classes.-noclassgc: desligava a coleta de classes não usadas.-verifyremote: ligava verificação só para código carregado via classloader não-system.
O comportamento novo é o que pega o time desprevenido. Não é mais um aviso amigável: agora a opção é tratada como desconhecida e o processo morre na largada.
$ java -noverify -jar app.jar
Unrecognized option: -noverify
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
Repara num detalhe que confunde muita gente: as variantes com prefixo X da HotSpot continuam existindo. -Xverify:remote e -Xnoclassgc seguem funcionando normalmente no JDK 27. Quem foi removido foi a forma curta do launcher (-noverify, -noclassgc) e o -Xverify:none. Então não adianta sair trocando tudo no susto: o que você precisa é caçar exatamente essas quatro strings.
Como Tech Leader, já vi um upgrade de Java travar uma esteira inteira por causa de uma flag que ninguém sabia que estava lá. A pessoa que colocou já tinha saído da empresa, o commit era de três anos atrás, e o motivo ("acelerar o build") nem fazia mais sentido. É o tipo de dívida silenciosa que só aparece quando o chão muda embaixo.
Por que a verificação de bytecode existe
Pra entender por que o JDK foi duro com essa flag, vale lembrar o que a verificação de bytecode faz. Quando a JVM carrega uma classe, o verificador confere que o bytecode é válido e seguro antes de executar: que a pilha de operandos não estoura, que os tipos batem em cada instrução, que não há salto pra fora do método, que um objeto não é usado antes de ser inicializado. É uma garantia estrutural que o compilador Java sozinho não dá, porque a JVM também roda classe que veio de outro lugar: biblioteca de terceiro, código gerado em runtime, classe baixada, framework que faz manipulação de bytecode (Hibernate, Spring, Mockito).
Quando alguém liga -noverify, essa rede some. Uma classe corrompida no disco, um JAR adulterado ou um bytecode gerado com bug passa direto, e em vez de um VerifyError claro no carregamento você ganha comportamento indefinido lá na frente: crash da JVM, corrupção de memória, falha silenciosa difícil de rastrear. O "ganho" de pular a verificação é da ordem de milissegundos no startup, e some completamente depois que as classes já foram carregadas. Trocar uma garantia de segurança permanente por alguns milissegundos de boot nunca foi um bom negócio, e o JDK 27 só formalizou isso tirando a opção da mesa.
Tem um detalhe de carreira aqui também. "Qual o papel do verificador de bytecode e por que você nunca deve desligá-lo em produção" é exatamente o tipo de pergunta que separa quem decora flags de quem entende a JVM numa entrevista sênior. Saber explicar isso vale mais que saber a flag.
Onde essa flag se esconde no seu projeto
O problema do -noverify não é achar, é que ele se espalha por lugares que você não olha no dia a dia. Os suspeitos de sempre:
- Dockerfile: no
ENTRYPOINT, noCMDou numa variávelJAVA_TOOL_OPTIONS. - Maven Surefire/Failsafe: no
argLinedos testes, pra "acelerar" a suíte. - Gradle: em
jvmArgsdo blocotestouapplication. - Run config de IDE: aquele "VM options" que alguém configurou no IntelliJ e commitou sem querer no
.idea. - Scripts de start:
.sh,.bat, manifests de Kubernetes, charts Helm.
O caso clássico é o Surefire. Um time grande resolve cortar o tempo de teste e alguém descobre que -noverify economiza alguns segundos por execução. Multiplica por milhares de builds e parece um ganho. Só que o que estava sendo cortado era a checagem de integridade do bytecode, justamente a rede de segurança que protege contra classe corrompida ou adulterada. O ganho era pequeno e o risco, grande. Agora o JDK 27 tira a decisão da sua mão.
Pra deixar concreto, é assim que a flag costuma aparecer no Gradle e no CI, e o estrago que ela faz quando está numa variável de ambiente global:
# Gradle (build.gradle) ERRADO:
test { jvmArgs '-noverify' }
# CERTO: remova a flag (ou troque por args ainda validos)
test { jvmArgs '-Xmx512m' }
# CI: a flag costuma se esconder dentro de JAVA_TOOL_OPTIONS
env:
JAVA_TOOL_OPTIONS: "-noverify -Xmx1g" # derruba TODO job no JDK 27
# a JVM le JAVA_TOOL_OPTIONS automaticamente, entao uma unica
# variavel global quebra build, teste e runtime de uma vez so
Esse último caso é o mais traiçoeiro. Como a JVM lê JAVA_TOOL_OPTIONS sozinha, uma única variável definida no runner do CI ou no namespace do Kubernetes derruba tudo que roda Java ali, do build ao container de produção, sem você ter tocado em nenhum arquivo do projeto.
Como varrer o repositório antes do upgrade
Antes de trocar a versão, faça uma varredura. Um grep recursivo nos formatos certos resolve a maior parte:
$ grep -rEn -- "-noverify|-Xverify:none|-noclassgc|-verifyremote" . \
--include="*.xml" --include="*.gradle" --include="*.kts" \
--include="*.yml" --include="*.yaml" --include="*.sh" \
--include="Dockerfile*" --include="*.properties"
# qualquer linha que aparecer aqui vai quebrar no JDK 27
O grep pega o que está versionado, mas não pega flag injetada em runtime (variável de ambiente no cluster, parâmetro do orquestrador). Pra fechar o cerco, pergunte direto pra JVM quais argumentos ela recebeu de fato, em qualquer ambiente:
import java.lang.management.ManagementFactory;
import java.util.List;
public class AuditarFlags {
public static void main(String[] args) {
List<String> jvmArgs =
ManagementFactory.getRuntimeMXBean().getInputArguments();
System.out.println("Flags da JVM: " + jvmArgs);
// procure por -noverify, -Xverify:none, -noclassgc, -verifyremote
}
}
Esse getInputArguments() devolve exatamente o que a JVM recebeu na linha de comando, então é a fonte da verdade. Rode num pod de homologação com a mesma configuração da produção antes de promover o JDK 27.
O fix (e por que -noverify nunca foi uma boa ideia)
O conserto, na maioria dos casos, é simplesmente apagar a flag. Não existe substituto pra -noverify, e isso é proposital: desligar a verificação de bytecode nunca foi recomendado fora de cenários muito específicos de debugging. No Dockerfile, o antes e depois é direto:
# ERRADO: quebra no JDK 27
ENTRYPOINT ["java", "-noverify", "-jar", "/app.jar"]
# CERTO: sem a flag, a verificacao roda e o app sobe
ENTRYPOINT ["java", "-jar", "/app.jar"]
Se a flag que estava lá era -noclassgc e você realmente precisa desligar a coleta de classes (raro, geralmente em apps que carregam classes dinamicamente e não querem o custo de descarregá-las), use a variante que sobrevive: -Xnoclassgc. Mas confirme que esse era mesmo o objetivo, porque quase sempre não era.
Tem um lugar que escapa do grep e pega muita gente: imagem construída com Cloud Native Buildpacks (o bootBuildImage do Spring Boot usa Paketo por baixo). O buildpack injeta JAVA_TOOL_OPTIONS em runtime, e se alguém colocou -noverify ali na configuração do builder, a flag não está em arquivo nenhum do seu repositório, mas chega na JVM do mesmo jeito. Nesses casos, o getInputArguments() rodando dentro do container é a única forma confiável de ver o que a aplicação recebeu de verdade. Não confie só no que está versionado.
Pra não depender de memória humana, vale colocar uma trava no startup da aplicação. Um self-check que falha rápido e deixa claro o motivo é melhor que um container que morre com mensagem críptica:
import java.lang.management.ManagementFactory;
import java.util.List;
public final class GuardaDeFlags {
private static final List<String> REMOVIDAS =
List.of("-noverify", "-Xverify:none", "-noclassgc", "-verifyremote");
public static void validar() {
ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
.filter(REMOVIDAS::contains)
.findFirst()
.ifPresent(flag -> {
throw new IllegalStateException(
"Flag removida no JDK 27 detectada: " + flag);
});
}
}
Chame GuardaDeFlags.validar() bem no começo do main, antes de subir o contexto do Spring. Aí, se alguém reintroduzir a flag num ambiente qualquer, a aplicação para com uma mensagem que diz exatamente o que fazer, em vez de uma falha genérica de boot.
Como impedir que a flag volte
Limpar uma vez não basta. Daqui a seis meses alguém copia um Dockerfile antigo de outro projeto e a flag volta sem ninguém perceber. A forma de garantir que isso não derrube produção de novo é transformar a regra num teste automático do próprio CI. Um passo simples que falha a pipeline se qualquer das quatro strings reaparecer:
# passo de guarda no CI (roda antes do build)
if grep -rEn -- "-noverify|-Xverify:none|-noclassgc|-verifyremote" . \
--include="*.xml" --include="*.gradle" --include="*.kts" \
--include="*.yml" --include="*.yaml" --include="Dockerfile*"; then
echo "ERRO: flag removida no JDK 27 encontrada acima"
exit 1
fi
echo "OK: nenhuma flag proibida no repositorio"
Esse passo custa segundos e paga por si na primeira vez que pega a flag antes de ela chegar em produção. É a diferença entre descobrir o problema no pull request, com o autor do commit ali do lado pra corrigir, e descobrir no deploy, às 18h de uma sexta, com o time inteiro tentando entender por que o container não sobe. Mover a descoberta do erro pra esquerda, pra mais perto de quem o introduziu, é o que separa uma esteira madura de uma que só apaga incêndio.
Resumo e próximo passo
Recapitulando o que importa antes de levar o JDK 27 pra produção:
- Quatro flags foram removidas:
-noverify,-Xverify:none,-noclassgce-verifyremote. Não são mais avisos, são erro fatal de boot. - As variantes com X continuam:
-Xverify:remotee-Xnoclassgcseguem válidas. Não troque no susto, cace as strings exatas. - Varra antes de migrar:
grepno repositório para o que está versionado,getInputArguments()em runtime para o que vem do ambiente.
E é isso. O JDK 27 não está te punindo, está só cobrando uma limpeza que devia ter sido feita lá atrás. Quem varre o projeto antes troca a versão num clique, quem confia que "sempre funcionou" descobre o problema com o deploy parado e o cliente esperando.
Você ainda tem -noverify em algum canto da sua stack? Já tomou susto com flag antiga que ninguém lembrava ter colocado? Conta nos comentários, quero ver os casos mais cabeludos. E se você está planejando o upgrade, dá uma olhada também no que o mesmo JDK 27 muda no seu Garbage Collector no artigo sobre G1GC virando default no container, e na economia de heap dos Compact Object Headers.
Perguntas frequentes (FAQ)
O -noverify foi mesmo removido ou só depreciado de novo?
Removido. No JDK 27 ele virou opção não reconhecida e a JVM falha ao iniciar. Antes (desde o Java 13) ele era aceito com um aviso de depreciação. Essa é a diferença que quebra builds: o que era warning agora é erro fatal.
Qual é o substituto de -noverify?
Não existe, e isso é de propósito. Desligar a verificação de bytecode não é recomendado em produção. Se o objetivo era o -noclassgc (desligar a coleta de classes), a variante -Xnoclassgc continua disponível no JDK 27.
-Xverify:none também foi removido? E -Xverify:remote?
-Xverify:none foi removido. Já -Xverify:remote continua funcionando normalmente. Só a forma que desligava totalmente a verificação saiu de cena.
Como descubro rápido se meu projeto usa alguma dessas flags?
Rode um grep recursivo procurando as quatro strings nos arquivos de build, Dockerfile, YAML e scripts. Para flags injetadas em runtime, use ManagementFactory.getRuntimeMXBean().getInputArguments() dentro da app, no ambiente real.
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, então o conjunto de mudanças, incluindo a remoção dessas flags, já está congelado. Dá pra testar agora pelo build de early access. Veja o aviso oficial no inside.java e nas release notes do JDK 27.
Vale a pena bloquear essas flags no CI mesmo antes de migrar pro JDK 27?
Vale. Mesmo no JDK 21 ou 17, -noverify já era uma má ideia de segurança. Bloquear agora no CI te deixa pronto pro JDK 27 sem pressa e remove um risco que já existia hoje. É barato e só tem a ganhar.
Quer ir mais fundo na evolução recente da JVM? Confira também o artigo sobre Spring AI na prática.