W3docs

Carregamento de Classes em Java

Como a JVM encontra e carrega classes com class loaders — bootstrap, platform, system e loaders personalizados.

Antes que a JVM possa executar uma única linha do seu código, ela precisa encontrar o arquivo .class, ler seu bytecode, verificá-lo e transformá-lo em um objeto Class vivo na memória. Essa tarefa pertence a um class loader. O carregamento de classes é o que torna java.lang.String disponível sem que você precise fazer nada, o que permite que um JAR no classpath apareça em tempo de execução, e o que alimenta sistemas de plugins, servidores de aplicação e ferramentas de hot-reload. Este capítulo mostra como os loaders são organizados, como funciona a delegação e por que a identidade de uma classe vai além do seu nome.

A hierarquia de class loaders

Os loaders são organizados como uma cadeia de pais, cada um responsável por uma fonte diferente de classes. Em um JDK moderno (9+), há três loaders integrados:

LoaderCarregaReportado como
BootstrapClasses principais do JDK (java.*, módulos base javax.*)null
PlatformO restante dos módulos de plataforma do JDKum PlatformClassLoader
System / ApplicationSeu código do classpath/module pathum AppClassLoader

Toda classe lembra o loader que a definiu. Você pode perguntar a qualquer classe qual loader a produziu:

ClassLoader appLoader = MyApp.class.getClassLoader();   // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader();  // null = bootstrap
ClassLoader parent    = appLoader.getParent();          // PlatformClassLoader

O loader bootstrap é escrito em código nativo, não em Java, por isso String.class.getClassLoader() retorna null em vez de um objeto — não existe uma instância Java de ClassLoader para devolver.

O modelo de delegação

Os class loaders seguem o modelo de delegação pai-primeiro. Quando solicitado a carregar uma classe, um loader não tenta encontrá-la imediatamente. Ele primeiro pergunta ao seu pai, que pergunta ao seu pai, até chegar ao bootstrap. Apenas se nenhum ancestral puder fornecer a classe é que o loader original tenta defini-la por conta própria.

// Conceptual shape of ClassLoader.loadClass:
protected Class<?> loadClass(String name, boolean resolve) {
  Class<?> c = findLoadedClass(name);     // already loaded? reuse it
  if (c == null) {
    try {
      c = parent.loadClass(name);         // delegate UP first
    } catch (ClassNotFoundException e) {
      c = findClass(name);                // only now load it myself
    }
  }
  return c;
}

Essa delegação garante que os tipos centrais sejam carregados uma vez, pelo loader mais alto que possa fornecê-los. É por isso que você não pode sobrescrever java.lang.String colocando seu próprio String.class no classpath — o loader bootstrap reclama o nome primeiro.

Carregamento, vinculação, inicialização

Trazer uma classe à vida acontece em três fases, e elas não são a mesma coisa:

  • Carregamento — ler o bytecode e criar o objeto Class.
  • Vinculaçãoverificar se o bytecode está bem formado, preparar campos estáticos com valores padrão e resolver referências simbólicas.
  • Inicialização — executar inicializadores estáticos e atribuições de campos estáticos (o método <clinit> da classe).

O fato prático mais importante: a inicialização é lazy e acontece exatamente uma vez. Uma classe só é inicializada no primeiro uso ativo — o primeiro new, a primeira chamada de método estático ou a primeira leitura de um campo estático não constante.

class Config {
  static final Map<String, String> SETTINGS = load();  // runs once, on first touch
  static Map<String, String> load() {
    System.out.println("Config initialized");
    return Map.of("env", "prod");
  }
}
// "Config initialized" prints only when Config is first actively used.

Class loaders personalizados

Você pode estender ClassLoader para carregar classes de qualquer lugar — um banco de dados, um fluxo de rede, bytecode gerado ou um JAR criptografado. Os dois métodos que importam são findClass (localizar e definir os bytes) e defineClass (entregar os bytes brutos para a JVM, que retorna um Class).

class BytesLoader extends ClassLoader {
  private final byte[] bytecode;
  BytesLoader(byte[] bytecode) { this.bytecode = bytecode; }
  @Override
  protected Class<?> findClass(String name) {
    return defineClass(name, bytecode, 0, bytecode.length);
  }
}

URLClassLoader é a versão integrada dessa ideia — aponte-o para JARs ou diretórios e ele carrega classes sob demanda:

URL jar = Path.of("plugin.jar").toUri().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
  Class<?> plugin = loader.loadClass("com.example.Plugin");
  Object instance = plugin.getDeclaredConstructor().newInstance();
}

Identidade de classe: nome mais loader

Aqui está a sutileza que pega as pessoas de surpresa: a identidade em tempo de execução de uma classe é seu nome totalmente qualificado e o loader que a definiu. Carregue bytes para Widget por meio de dois loaders diferentes e você obtém dois objetos Class distintos — não iguais, não compatíveis por atribuição — mesmo que ambos tenham vindo de bytecode idêntico. É exatamente assim que os servidores de aplicação isolam dois aplicativos implantados que carregam uma classe chamada com.acme.Util.

Um exemplo prático: loaders, delegação, lazy e identidade

Este programa não precisa de classes externas — ele usa os loaders já presentes em qualquer JVM. Ele percorre a cadeia de loaders, prova que classes principais vêm do loader bootstrap, mostra a delegação retornando o mesmo objeto Class, observa um inicializador estático ser disparado de forma lazy e apenas uma vez, e depois define o mesmo bytecode construído manualmente por dois loaders para provar a regra de identidade nome-mais-loader.

java— editable, runs on the server

O que observar na execução:

  • A cadeia de loaders impressa é a hierarquia de class loaders ativa com seu código na base: ClassLoadingDemo foi definido por um loader de nível de aplicação cujo getParent() é o próximo loader acima. Cada loader conhece apenas o seu pai, e a cadeia sempre sobe em direção ao bootstrap.
  • String.class.getClassLoader() imprime null, a forma da JVM de dizer "carregado pelo loader bootstrap". Tipos principais do JDK sempre reportam null aqui; um objeto implicaria que vieram de um loader inferior, o que nunca acontece.
  • app.loadClass("java.lang.StringBuilder") == StringBuilder.class é true. A delegação enviou a solicitação para o loader que já possui StringBuilder, então você recebeu de volta o objeto Class idêntico, não uma duplicata — prova de que a delegação impede que os tipos centrais sejam carregados duas vezes.
  • Lazy <clinit> running é impresso uma vez, entre o marcador --- referencing Lazy now --- e o primeiro Lazy.VALUE = 42, e nunca mais na segunda leitura. A inicialização é lazy (esperou até o primeiro uso) e idempotente (o bloco estático executa exatamente uma vez por loader).
  • a e b têm ambos o nome Widget, mas a == b é false e a.isAssignableFrom(b) é false. Dois loaders definiram o mesmo bytecode em dois tipos distintos — prova concreta de que a identidade de classe em tempo de execução é nome totalmente qualificado mais loader definidor, o mecanismo por trás do isolamento de classpath em servidores de aplicação.

Prática

Prática
Dois class loaders personalizados separados carregam bytecode idêntico para uma classe chamada 'com.acme.Widget'. O que é verdade sobre os objetos Class resultantes a (do loader 1) e b (do loader 2)?
Dois class loaders personalizados separados carregam bytecode idêntico para uma classe chamada 'com.acme.Widget'. O que é verdade sobre os objetos Class resultantes a (do loader 1) e b (do loader 2)?

Tópicos relacionados

O carregamento de classes fica na fronteira entre a JVM e o seu código, por isso toca vários tópicos vizinhos:

  • Arquitetura da JVM — onde o subsistema de class loader se encaixa entre o motor de execução e as áreas de dados em tempo de execução.
  • Modelo de Memória Java — como as classes carregadas e seus dados estáticos vivem na memória.
  • Coleta de Lixo — os class loaders (e suas classes) podem ser descarregados quando não há mais referências a eles.
  • Introdução a Módulos — no module path, o carregamento é orientado pela legibilidade de módulos em vez de um classpath plano.
  • Introdução a ReflectionClass.forName e loadClass são os pontos de entrada sobre os quais a reflection se baseia.
Was this page helpful?