Como entregar um programa Java a quem não tem Java instalado: descobrir de quais módulos ele depende, montar um Java sob medida com jlink, gerar instaladores com jpackage, acelerar a inicialização com os caches do Projeto Leyden e compilar tudo para código nativo com o GraalVM.
Tudo o que este curso construiu até aqui roda com um comando que começa com java. Isso pressupõe que a pessoa do outro lado tem o Java instalado, e na versão certa. Durante vinte anos, essa pressuposição era razoável: a Sun, e depois a Oracle, distribuíam o JRE (Java Runtime Environment), a parte do Java necessária só para executar programas, e ele vinha instalado em boa parte dos computadores, junto com o plugin que rodava os applets nos navegadores. Esse mundo acabou. Os navegadores abandonaram os plugins em meados da década de 2010, o Java 9 descontinuou os applets e o Java 11, em 2018, removeu o Java Web Start e deixou de ter um JRE separado distribuído pela Oracle. A mensagem do próprio JDK desde então é clara: a aplicação deve levar o seu próprio Java. Esta aula mostra as ferramentas para isso, e fecha a Fase 8.
O ponto de partida
O exemplo desta aula é uma pequena agenda, organizada como um módulo, no formato de "Fronteiras Explícitas":
// module-info.java
module br.com.exemplo.agenda {
requires java.net.http;
requires java.logging;
}
package br.com.exemplo.agenda;
import java.net.http.HttpClient;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.util.Locale;
import java.util.logging.Logger;
public class Principal {
private static final Logger LOG = Logger.getLogger(Principal.class.getName());
public static void main(String[] args) {
LocalDate hoje = LocalDate.now();
String dia = hoje.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.of("pt", "BR"));
System.out.println("Agenda da Ilha — hoje é " + dia + ", " + hoje);
System.out.println("Cliente HTTP disponível: " + HttpClient.newHttpClient().version());
System.out.println("Java " + Runtime.version() + " em " + System.getProperty("java.home"));
LOG.fine("fim");
}
}
Compilada e empacotada num JAR executável, como em "Do Arquivo Solto ao Projeto":
$ javac -d mods/br.com.exemplo.agenda $(find src -name '*.java')
$ jar --create --file agenda.jar --main-class br.com.exemplo.agenda.Principal -C mods/br.com.exemplo.agenda .
$ java -jar agenda.jar
Agenda da Ilha — hoje é sexta-feira, 2026-09-25
Cliente HTTP disponível: HTTP_2
Java 25.0.4.1+1-1-24.04.4-Ubuntu em /usr/lib/jvm/java-25-openjdk-amd64
O JAR tem 2,4 KB. O JDK que o executa ocupa 330 MB. Para entregar esse programa a alguém, a pessoa precisaria instalar esses 330 MB, na versão 25 ou superior, e saber digitar java -jar. Para uma biblioteca, publicada num repositório Maven, o JAR é a entrega certa, porque quem a usa já é programador e já tem Java. Para uma aplicação, é pouco.
Descobrindo as dependências: jdeps
O primeiro passo para levar "só o Java necessário" é saber o que é necessário. O Sistema de Módulos de "Fronteiras Explícitas" dividiu o JDK em cerca de setenta módulos, e a ferramenta jdeps analisa o bytecode de um JAR e lista de quais deles o código depende:
$ jdeps --print-module-deps agenda.jar
java.base,java.logging,java.net.http
$ jdeps -s agenda.jar
br.com.exemplo.agenda -> java.base
br.com.exemplo.agenda -> java.logging
br.com.exemplo.agenda -> java.net.http
Para um JAR modular, a resposta confirma o module-info.java. O jdeps é mais útil com JARs não modulares, a maioria das aplicações e bibliotecas, porque descobre as dependências lendo cada classe usada. Ele também detecta o uso de APIs internas do JDK, como sun.misc.Unsafe, que podem quebrar em versões futuras, com a opção --jdk-internals.
jlink: um Java sob medida
O jlink, introduzido no Java 9 junto com os módulos, monta uma imagem de execução contendo apenas os módulos pedidos, com uma JVM e um comando de lançamento:
$ jlink --module-path agenda.jar \
--add-modules br.com.exemplo.agenda \
--launcher agenda=br.com.exemplo.agenda/br.com.exemplo.agenda.Principal \
--strip-debug --no-man-pages --no-header-files --compress zip-6 \
--output imagem
$ du -sh imagem
42M imagem
$ imagem/bin/java --list-modules
br.com.exemplo.agenda
java.base@25.0.4.1
java.logging@25.0.4.1
java.net.http@25.0.4.1
O --add-modules indica o módulo raiz, e o jlink segue os requires para incluir tudo o que ele precisa. O --launcher cria um script bin/agenda que já sabe qual classe executar. As outras opções reduzem o tamanho: --strip-debug remove as informações de depuração do bytecode, as duas seguintes descartam a documentação e os cabeçalhos para C, e --compress zip-6 comprime os módulos. O resultado é uma pasta de 42 MB, que roda em qualquer máquina com o mesmo sistema operacional e arquitetura, sem Java instalado. Para comparação, uma imagem com todos os módulos do JDK, feita com o mesmo jlink, ocupa cerca de 100 MB.
Mas executar a imagem revela uma surpresa:
$ imagem/bin/agenda
Agenda da Ilha — hoje é Fri, 2026-09-25
O dia da semana saiu em inglês. O nome "sexta-feira" não está no código, nem em java.base: os dados de idiomas diferentes do inglês ficam no módulo jdk.localedata, que o java.base carrega como um serviço, com o ServiceLoader de "Fronteiras Explícitas". Como nenhum requires aponta para ele, nem o jdeps nem o jlink o encontram, e o programa cai silenciosamente para o inglês. A correção é incluí-lo à mão, e, para não levar os dados de todos os idiomas do mundo, filtrá-los:
$ jlink --module-path agenda.jar \
--add-modules br.com.exemplo.agenda,jdk.localedata --include-locales=pt \
--launcher agenda=br.com.exemplo.agenda/br.com.exemplo.agenda.Principal \
--strip-debug --no-man-pages --no-header-files --compress zip-6 \
--output imagem
$ du -sh imagem
43M imagem
$ imagem/bin/agenda
Agenda da Ilha — hoje é sexta-feira, 2026-09-25
Com todos os idiomas, a imagem teria 53 MB; com o português, 43. O padrão aceito pelo --include-locales segue as marcas de idioma do CLDR, e neste JDK pt funciona enquanto pt-BR não encontra nada, porque os dados do português do Brasil são o próprio pt. Essa armadilha, dependências carregadas como serviço que nenhuma análise estática enxerga, é a mais comum ao usar o jlink. Outros exemplos são jdk.charsets, com codificações de texto menos comuns, e jdk.zipfs, o sistema de arquivos para ler um ZIP como uma pasta. A opção --bind-services inclui todos os provedores de serviço disponíveis, ao custo de uma imagem bem maior. A única defesa confiável é testar a imagem, e não o JAR.
jpackage: um instalador de verdade
Uma pasta com um script ainda não é o que um usuário comum espera. O jpackage, finalizado no Java 16, usa o jlink por baixo e produz um pacote nativo do sistema operacional: .msi ou .exe no Windows, .dmg ou .pkg no macOS, .deb ou .rpm no Linux, com ícone, atalho no menu e desinstalador. O tipo mais simples é a imagem de aplicação, uma pasta pronta para ser compactada e distribuída:
$ jpackage --type app-image --name Agenda --app-version 1.0.0 \
--module-path agenda.jar \
--module br.com.exemplo.agenda/br.com.exemplo.agenda.Principal \
--add-modules jdk.localedata \
--jlink-options "--strip-debug --no-man-pages --no-header-files --compress zip-6 --include-locales=pt" \
--dest pacote
$ find pacote -maxdepth 3
pacote/Agenda
pacote/Agenda/bin/Agenda
pacote/Agenda/lib/runtime
pacote/Agenda/lib/app
pacote/Agenda/lib/Agenda.png
pacote/Agenda/lib/libapplauncher.so
$ pacote/Agenda/bin/Agenda
Agenda da Ilha — hoje é sexta-feira, 2026-09-25
Agora o lançador bin/Agenda é um executável nativo, e não um script, e a imagem de execução fica em lib/runtime. Trocando --type app-image por --type deb, msi ou dmg, o jpackage gera o instalador correspondente, com opções como --icon, --win-menu, --win-shortcut e --linux-shortcut. Dois detalhes práticos: cada tipo de pacote precisa ser gerado no próprio sistema operacional a que se destina, porque o jpackage usa as ferramentas nativas de cada um (o WiX no Windows, o fakeroot e o dpkg-deb para .deb no Linux), e por isso projetos multiplataforma geram seus instaladores num serviço de integração contínua com máquinas de cada sistema. Numa máquina Linux com o fakeroot e o dpkg-deb instalados, o .deb da agenda, com a imagem de execução embutida, ficou com 13 MB. E instaladores para Windows e macOS distribuídos publicamente precisam ser assinados com um certificado, ou os sistemas avisam o usuário de que o programa é de origem desconhecida.
O jpackage é o caminho para aplicações de desktop, como as da Trilha B: o usuário baixa, instala com dois cliques e nunca fica sabendo que o programa foi escrito em Java.
Inicialização mais rápida: CDS e o cache AOT
Tamanho não é a única preocupação. Uma ferramenta de linha de comando executada milhares de vezes por dia, ou uma função em nuvem que sobe a cada requisição, sofre com o tempo de inicialização da JVM: carregar e verificar centenas de classes, interpretá-las, esperar o JIT de "A Máquina por Trás da Máquina" aquecer. Medindo a agenda, a mediana de seis execuções:
java -jar agenda.jar mediana 625 ms, pico de memória 73 MB
java -Xshare:off -jar agenda.jar mediana 719 ms, pico de memória 71 MB
imagem/bin/agenda mediana 735 ms, pico de memória 78 MB
A segunda linha desliga o CDS (Class Data Sharing), um recurso que o JDK usa por padrão desde o Java 12: um arquivo, lib/server/classes.jsa, com as classes mais comuns do JDK já carregadas e verificadas, mapeado direto na memória na inicialização. Sem ele, a agenda leva 100 ms a mais. E a imagem do jlink ficou mais lenta que o JDK completo, pelo mesmo motivo: ela não inclui esse arquivo. O jlink pode gerá-lo, com a opção --generate-cds-archive, e a imagem cai para 660 ms, ao preço de subir de 43 para 70 MB. Nenhuma otimização é de graça.
O Projeto Leyden, do OpenJDK, estende essa ideia. O Java 24 trouxe o cache AOT (JEP 483), que guarda não só as classes do JDK, mas também as da aplicação, já carregadas e ligadas, observadas numa execução de treino. O Java 25 simplificou a criação para um único passo (JEP 514) e passou a incluir no cache os perfis de execução dos métodos (JEP 515), para que o JIT comece a otimizar mais cedo:
$ java -XX:AOTCacheOutput=agenda.aot -jar agenda.jar # execução de treino: grava o cache
$ ls -la agenda.aot
-rw-r--r-- 1 root root 22343680 Sep 25 18:33 agenda.aot
$ java -XX:AOTCache=agenda.aot -jar agenda.jar # execuções seguintes usam o cache
java -XX:AOTCache=agenda.aot -jar agenda.jar mediana 565 ms, pico de memória 67 MB
O ganho aqui foi de 10%, de 625 para 565 ms, e a memória também caiu. Parece pouco, e a razão é uma lição de "Medir Antes de Otimizar": cronometrando cada linha do main, a criação do HttpClient sozinha leva cerca de 210 ms, porque inicializa a infraestrutura de criptografia para HTTPS, e isso nenhum cache de classes resolve. Um "olá, mundo" na mesma máquina inicia em 32 ms. Em aplicações com muitas classes, como as feitas com frameworks, o cache AOT costuma reduzir a inicialização de forma bem mais expressiva, e as JEPs do Leyden relatam ganhos da ordem de 40% em casos assim. O cache deve ser gerado com a mesma versão do JDK e o mesmo classpath da produção, e uma execução de treino que exercite o caminho real do programa produz um cache melhor.
GraalVM Native Image: sem JVM nenhuma
A abordagem mais radical é abandonar a JVM na execução. O GraalVM, criado na Oracle Labs a partir de um projeto de pesquisa sobre compiladores escritos em Java, tem uma ferramenta chamada Native Image, que compila uma aplicação Java inteira, junto com as partes necessárias da biblioteca padrão e um coletor de lixo simplificado, num executável nativo do sistema operacional:
$ native-image -H:IncludeLocales=pt -jar agenda.jar -o agenda
...
Finished generating 'agenda' in 41.6s.
$ ./agenda
Agenda da Ilha — hoje é quinta-feira, 2026-10-08
Cliente HTTP disponível: HTTP_2
Java 25.0.4.1.1+1-jvmci-25.4-b23 em null
Duas linhas da saída merecem atenção. O -H:IncludeLocales=pt existe pelo mesmo motivo do jdk.localedata no jlink: sem ele, o executável leva só os dados do idioma padrão, e o dia sai como "Thu". E o java.home é null, porque não existe nenhuma instalação de Java por trás do programa.
A compilação é antecipada (ahead-of-time, AOT), ao contrário do JIT. Para isso, o Native Image faz uma análise de alcançabilidade: parte do main e descobre todo o código que pode ser executado, descartando o resto. Essa análise pressupõe um mundo fechado: tudo o que o programa vai usar precisa ser conhecido na compilação.
As consequências são as vantagens e os custos da abordagem. O executável inicia em milissegundos, porque não há classes para carregar nem código para interpretar, usa bem menos memória e não precisa de nenhum Java instalado. Em troca, a compilação demora minutos e consome vários gigabytes de memória, o executável só roda no sistema e arquitetura em que foi gerado, e o desempenho de pico em execuções longas tende a ser menor que o do JIT, que otimiza com base no comportamento real do programa (a versão comercial do GraalVM oferece otimização guiada por perfil para reduzir essa diferença).
O custo mais sério é o mundo fechado. Tudo o que é dinâmico no Java, a reflexão de "O Programa que se Examina", os proxies dinâmicos, o carregamento de recursos por nome, a serialização, é invisível para a análise estática. Uma classe carregada com Class.forName(nomeLidoDeUmArquivo) funciona com a JVM e falha no executável nativo, porque a classe nem foi incluída. A solução é declarar esses usos em arquivos de metadados de alcançabilidade (reachability-metadata.json, dentro de META-INF/native-image), que podem ser gerados executando o programa com um agente de rastreamento, java -agentlib:native-image-agent=config-output-dir=..., que registra toda reflexão feita durante a execução. Frameworks como Quarkus, Micronaut e Spring Boot geram esses metadados automaticamente, e é por isso que o Native Image se popularizou justamente com eles, em microsserviços e funções em nuvem, onde a inicialização rápida e a memória baixa se traduzem em custo menor.
Na mesma máquina das outras medições, com o GraalVM Community para o Java 25, a compilação da agenda levou 42 segundos e chegou a 1,6 GB de memória, o executável ficou com 19 MB, e a agenda passou a iniciar em 6 ms, contra cerca de 600 ms do java -jar. Uma nota sobre o futuro: em 2025, a Oracle anunciou que o GraalVM deixaria de acompanhar o ritmo de versões do Java SE e concentraria esforços em outras frentes, enquanto a evolução da compilação antecipada dentro do próprio JDK ficaria a cargo do Projeto Leyden. O Native Image continua disponível e usado, mas vale verificar o estado atual do projeto antes de apostar nele.
Qual escolher
As quatro formas de entrega atendem necessidades diferentes, e a escolha segue quem vai receber o programa:
- JAR: para bibliotecas e para quem já tem Java, como ferramentas internas de uma equipe. É a entrega universal dentro do ecossistema.
- Imagem do jlink: para servidores, especialmente dentro de contêineres Docker, como na Trilha A, onde uma imagem de 43 MB no lugar de 330 MB reduz o tempo de publicação e a superfície de ataque. Combina bem com o cache AOT.
- Instalador do jpackage: para aplicações de desktop entregues a usuários finais, que esperam um instalador comum.
- Executável nativo: para ferramentas de linha de comando executadas com muita frequência e serviços em nuvem que sobem e descem o tempo todo, quando o custo do mundo fechado compensa.
Com esta aula termina a oitava fase, que tratou do que acontece ao redor do código que funciona. Em "Lições de Quem Veio Antes", você leu três livros clássicos com olhar crítico: aprendeu a criar objetos com fábricas estáticas e Builders, viu a herança quebrar um contador que a composição mantinha correto, reencontrou os padrões Strategy, Observer e Decorator reduzidos a lambdas e records, e reescreveu um método ilegível com nomes que revelam intenção, desconfiando de regras aplicadas sem motivo. Em "Contratos à Vista", protegeu objetos com imutabilidade e cópias defensivas, viu um período terminar antes de começar e aprendeu a copiar antes de validar, falhou cedo com validações na fronteira e declarou com o JSpecify onde o null é permitido, para que um verificador o confira antes de o programa rodar. E nesta aula, entregou um programa a quem não tem Java, com o jdeps, o jlink e o jpackage, mediu a inicialização e descobriu que as ferramentas de aceleração só ajudam onde o tempo realmente está.
O que as três aulas têm em comum é a mudança de ponto de vista: da pessoa que escreve o código para as pessoas que vão lê-lo, usá-lo e recebê-lo. As Fases 1 a 7 ensinaram a linguagem e a plataforma; a Fase 8 ensinou a responsabilidade que vem junto. Com isso, o núcleo do curso está completo, e é hora de juntar tudo num único projeto.
Fontes e leituras recomendadas
- Oracle, "The jlink Command" (Java SE 25 Tools Reference): todas as opções do
jlink, incluindo--include-locales,--bind-servicese--generate-cds-archive. https://docs.oracle.com/en/java/javase/25/docs/specs/man/jlink.html - Oracle, "Packaging Tool User's Guide" (Java SE 25): o guia do
jpackage, com os requisitos de cada sistema operacional e as opções de personalização dos instaladores. https://docs.oracle.com/en/java/javase/25/jpackage/ - JEP 483, "Ahead-of-Time Class Loading & Linking", e JEP 514, "Ahead-of-Time Command-Line Ergonomics": as propostas do Projeto Leyden que criaram e simplificaram o cache AOT, com números de ganho em aplicações reais. https://openjdk.org/jeps/483 e https://openjdk.org/jeps/514
- GraalVM, "Native Image" (documentação): o funcionamento da análise de alcançabilidade, os metadados para reflexão e o agente de rastreamento. https://www.graalvm.org/latest/reference-manual/native-image/
- Oracle, "The jdeps Command" (Java SE 25 Tools Reference): as opções de análise de dependências, incluindo
--print-module-depse--jdk-internals. https://docs.oracle.com/en/java/javase/25/docs/specs/man/jdeps.html
Exercícios
Exercício 1
Explique por que a primeira imagem do jlink mostrou "Fri" em vez de "sexta-feira", por que o jdeps não avisou, e o que isso ensina sobre como validar uma imagem.
Ver resposta
✓ Resposta: O nome do dia da semana em português está nos dados de idioma do módulo jdk.localedata. O java.base não depende dele por um requires; ele o encontra em tempo de execução pelo ServiceLoader, procurando provedores de dados de idioma entre os módulos presentes. Na imagem mínima, o módulo não estava, o ServiceLoader não achou nenhum provedor, e o Java usou os dados embutidos em java.base, que só cobrem o inglês, sem nenhum erro. O jdeps analisa o bytecode e encontra dependências estáticas, as classes referenciadas diretamente; uma dependência de serviço é resolvida em tempo de execução, por nome, e não aparece no bytecode de quem a usa. A lição é que a imagem gerada é um produto diferente do JAR, e precisa ser testada como tal: a mesma bateria de testes, ou pelo menos um teste de fumaça que exercite os recursos que dependem de serviços, como idiomas, codificações e sistemas de arquivos, deve rodar sobre a imagem, e não sobre o JDK de desenvolvimento.
Exercício 2
Um programa lê arquivos de dentro de um ZIP com FileSystems.newFileSystem(caminhoDoZip). Funciona com java -jar, mas numa imagem do jlink lança ProviderNotFoundException. Por quê? Dê duas correções e compare-as.
Ver resposta
✓ Resposta: O suporte a ZIP como sistema de arquivos está no módulo jdk.zipfs, que registra um FileSystemProvider como serviço. O código chama uma API de java.base, que procura provedores pelo ServiceLoader, e o jdeps não enxerga o jdk.zipfs, pelo mesmo motivo do exercício anterior. Na imagem mínima, nenhum provedor para ZIP existe, e a exceção é lançada. A primeira correção é acrescentar o módulo explicitamente: --add-modules br.com.exemplo.app,jdk.zipfs. É precisa, e a imagem cresce muito pouco. A segunda é usar --bind-services, que faz o jlink incluir todos os módulos que fornecem serviços usados pelos módulos da imagem. É automática e resolve casos que ninguém previu, mas inclui dezenas de módulos desnecessários, e a imagem pode multiplicar de tamanho. A primeira é a melhor prática, desde que acompanhada de um comentário no script de construção explicando por que o módulo está ali, e de um teste que exercite a leitura de ZIP sobre a imagem.
Exercício 3
A imagem do jlink iniciou em 735 ms, mais devagar que o java -jar com o JDK completo, em 625 ms. Explique a diferença e diga como recuperá-la, com o custo de cada opção.
Ver resposta
✓ Resposta: O JDK completo traz um arquivo CDS, lib/server/classes.jsa, com centenas de classes do JDK já carregadas, verificadas e prontas para serem mapeadas na memória na inicialização. A imagem do jlink, por padrão, não traz esse arquivo, porque ele depende exatamente dos módulos incluídos, e a JVM precisa carregar e verificar cada classe do zero. O efeito é o mesmo de rodar o JDK completo com -Xshare:off, que deu 719 ms. A primeira forma de recuperar é gerar o arquivo com --generate-cds-archive no jlink: a inicialização caiu para 660 ms, mas a imagem cresceu de 43 para 70 MB, porque o arquivo CDS guarda as classes numa forma pronta para uso, maior que a comprimida. A segunda é criar um cache AOT da aplicação com uma execução de treino, -XX:AOTCacheOutput, e usá-lo com -XX:AOTCache, o que cobre também as classes da aplicação; o custo é um arquivo de uns 20 MB, que precisa ser gerado de novo a cada versão da aplicação ou do JDK. A escolha depende do que pesa mais: num contêiner que sobe raramente, os 30 MB a mais não compensam 75 ms; numa função em nuvem que inicia a cada requisição, compensam.
Exercício 4
Escolha a forma de entrega mais adequada para cada caso e justifique: (a) uma ferramenta de linha de comando que valida arquivos de configuração, executada milhares de vezes por dia em pipelines de integração contínua; (b) um editor de mapas para jogos, usado por artistas que não são programadores; (c) a API das Trilhas da Ilha, publicada num contêiner Docker e em execução contínua; (d) uma biblioteca de validação de CPF usada por outros projetos Java.
Ver resposta
✓ Resposta: (a) Um executável nativo com o GraalVM, se a ferramenta não depender de reflexão difícil de declarar: executada milhares de vezes, cada uma por poucos segundos, ela passa a maior parte do tempo inicializando, e um executável que inicia em milissegundos, sem precisar de Java no ambiente de CI, economiza tempo em todas as execuções. Se o mundo fechado for um problema, a alternativa é uma imagem do jlink com cache AOT. (b) Um instalador do jpackage, gerado para cada sistema operacional dos artistas: eles esperam baixar, instalar com dois cliques e encontrar o programa no menu, sem saber o que é Java. Com assinatura digital, para que o sistema não o trate como suspeito. (c) Uma imagem do jlink dentro do contêiner, ou uma imagem base com o JDK já pronta: o serviço fica no ar por dias, então a inicialização pesa pouco, e o JIT, com desempenho de pico superior em execuções longas, é uma vantagem; a imagem reduzida diminui o tamanho do contêiner e o número de componentes a manter atualizados. É o que a Trilha A vai fazer. (d) Um JAR comum, publicado num repositório Maven: quem usa uma biblioteca já tem Java e um sistema de construção, e embutir um Java nela não faria sentido. O cuidado aqui é outro: ter um module-info.java ou pelo menos um nome de módulo automático, e anotar a API com JSpecify.
Exercício 5
Um sistema de relatórios carrega "plugins" lendo nomes de classes de um arquivo de configuração e instanciando-os com Class.forName(nome).getDeclaredConstructor().newInstance(). Explique o que acontece ao compilá-lo com o GraalVM Native Image e como resolver.
Ver resposta
✓ Resposta: A análise de alcançabilidade parte do main e segue as referências estáticas do código. As classes dos plugins nunca são referenciadas diretamente: seus nomes só existem num arquivo lido em tempo de execução. A análise não tem como saber quais classes serão pedidas, então elas não são incluídas no executável, ou são incluídas sem os metadados de reflexão necessários para encontrar o construtor. Em tempo de execução, o Class.forName lança ClassNotFoundException, ou o getDeclaredConstructor falha. Há três caminhos. O primeiro é declarar os plugins nos metadados de alcançabilidade, registrando cada classe e seu construtor para reflexão; o agente de rastreamento pode gerar esses metadados automaticamente se o programa for executado com todos os plugins em uso. O segundo é redesenhar o carregamento com o ServiceLoader, declarando os plugins em META-INF/services ou nos provides de um module-info.java: o Native Image registra automaticamente os provedores de serviço que encontra durante a construção, e o desenho fica mais limpo também na JVM. O terceiro é aceitar a limitação: se o objetivo do sistema é justamente carregar plugins desconhecidos em tempo de execução, escritos depois da compilação, o mundo fechado é incompatível com ele, e o Native Image não é a ferramenta certa; o cache AOT do Leyden, que mantém a JVM completa, seria a alternativa para acelerar a inicialização.