JavaRush /Blogue Java /Random-PT /Como as classes são carregadas na JVM
Aleksandr Zimin
Nível 1
Санкт-Петербург

Como as classes são carregadas na JVM

Publicado no grupo Random-PT
Depois de concluída a parte mais difícil do trabalho de um programador e escrita a aplicação “Hello World 2.0”, resta montar o kit de distribuição e transferi-lo para o cliente, ou pelo menos para o serviço de testes. Na distribuição está tudo como deveria estar, e quando lançamos nosso programa, a Java Virtual Machine entra em cena. Não é nenhum segredo que a máquina virtual lê comandos apresentados em arquivos de classe na forma de bytecode e os traduz como instruções para o processador. Proponho entender um pouco sobre o esquema de entrada de bytecode na máquina virtual.

Carregador de classes

É utilizado para fornecer bytecode compilado à JVM, que geralmente é armazenado em arquivos com extensão .class, mas também pode ser obtido de outras fontes, por exemplo, baixado pela rede ou gerado pela própria aplicação. Como as classes são carregadas na JVM – 1De acordo com a especificação Java SE, para executar o código na JVM, você precisa concluir três etapas:
  • carregando bytecode de recursos e criando uma instância da classeClass

    Isso inclui procurar a classe solicitada entre aquelas carregadas anteriormente, obter bytecode para carregar e verificar sua exatidão, criar uma instância da classe Class(para trabalhar com ela em tempo de execução) e carregar classes pai. Se as classes e interfaces pai não tiverem sido carregadas, a classe em questão será considerada não carregada.

  • vinculação (ou vinculação)

    De acordo com a especificação, esta etapa é dividida em mais três etapas:

    • Verificação , a exatidão do bytecode recebido é verificada.
    • Preparação , alocando RAM para campos estáticos e inicializando-os com valores padrão (neste caso, a inicialização explícita, se houver, ocorre já na fase de inicialização).
    • Resolução , resolução de links simbólicos de tipos, campos e métodos.
  • inicializando o objeto recebido

    aqui, ao contrário dos parágrafos anteriores, tudo parece claro sobre o que deve acontecer. Seria, claro, interessante entender exatamente como isso acontece.

Todas essas etapas são executadas sequencialmente com os seguintes requisitos:
  • A classe deve estar totalmente carregada antes de ser vinculada.
  • Uma classe deve ser totalmente testada e preparada antes de ser inicializada.
  • Erros de resolução de link ocorrem durante a execução do programa, mesmo que tenham sido detectados na fase de linkagem.
Como você sabe, Java implementa carregamento lento (ou lento) de classes. Isso significa que o carregamento das classes dos campos de referência da classe carregada não será realizado até que a aplicação encontre uma referência explícita a eles. Em outras palavras, a resolução de links simbólicos é opcional e não ocorre por padrão. No entanto, a implementação da JVM também pode usar carregamento de classe energético, ou seja, todos os links simbólicos devem ser levados em consideração imediatamente. É para este ponto que se aplica o último requisito. Vale ressaltar também que a resolução dos links simbólicos não está vinculada a nenhuma etapa do carregamento da classe. Em geral, cada uma destas etapas é um bom estudo; vamos tentar descobrir a primeira, nomeadamente carregar o bytecode.

Tipos de carregadores Java

Existem três carregadores padrão em Java, cada um carregando uma classe de um local específico:
  1. Bootstrap é um carregador básico, também chamado Primordial ClassLoader.

    carrega classes JDK padrão do arquivo rt.jar

  2. Extensão ClassLoader – carregador de extensão.

    carrega classes de extensão, que estão localizadas no diretório jre/lib/ext por padrão, mas podem ser definidas pela propriedade do sistema java.ext.dirs

  3. System ClassLoader – carregador do sistema.

    carrega classes de aplicativos definidas na variável de ambiente CLASSPATH

Java usa uma hierarquia de carregadores de classe, onde a raiz é, obviamente, a base. Em seguida vem o carregador de extensão e depois o carregador do sistema. Naturalmente, cada carregador armazena um ponteiro para o pai para poder delegar o carregamento a ele, caso ele próprio não consiga fazer isso.

Classe abstrata ClassLoader

Cada carregador, com exceção do base, é descendente da classe abstrata java.lang.ClassLoader. Por exemplo, a implementação do carregador de extensão é a classe sun.misc.Launcher$ExtClassLoadere o carregador do sistema é sun.misc.Launcher$AppClassLoader. O carregador base é nativo e sua implementação está incluída na JVM. Qualquer classe que se estenda java.lang.ClassLoaderpode fornecer sua própria maneira de carregar aulas com blackjack e essas mesmas. Para isso, é necessário redefinir os métodos correspondentes, que neste momento só posso considerar superficialmente, porque Não entendi esse problema em detalhes. Aqui estão eles:
package java.lang;
public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}
loadClass(String name)um dos poucos métodos públicos, que é o ponto de entrada para carregar classes. Sua implementação se resume a chamar outro método protegido loadClass(String name, boolean resolve), que precisa ser sobrescrito. Se você olhar o Javadoc deste método protegido, poderá entender algo como o seguinte: dois parâmetros são fornecidos como entrada. Um é o nome binário da classe (ou nome de classe totalmente qualificado) que precisa ser carregado. O nome da classe é especificado com uma lista de todos os pacotes. O segundo parâmetro é um sinalizador que determina se a resolução do link simbólico é necessária. Por padrão é false , o que significa que o carregamento lento da classe é usado. Além disso, de acordo com a documentação, na implementação padrão do método, é feita uma chamada findLoadedClass(String name)que verifica se a classe já foi carregada anteriormente e, em caso afirmativo, retorna uma referência a esta classe. Caso contrário, o método de carregamento de classe do carregador pai será chamado. Se nenhum dos carregadores conseguir encontrar uma classe carregada, cada um deles, seguindo na ordem inversa, tentará encontrar e carregar essa classe, substituindo o arquivo findClass(String name). Isso será discutido com mais detalhes no capítulo “Esquema de carregamento de classes”. E por último, mas não menos importante, após o carregamento da classe, dependendo do sinalizador de resolução , será decidido se deseja carregar as classes através de links simbólicos. Um exemplo claro é que o estágio Resolução pode ser chamado durante o estágio de carregamento da classe. Da mesma forma, ao estender a classe ClassLoadere substituir seus métodos, o carregador personalizado pode implementar sua própria lógica para entregar bytecode à máquina virtual. Java também suporta o conceito de carregador de classes "atual". O carregador atual é aquele que carregou a classe em execução no momento. Cada classe sabe com qual carregador foi carregada e você pode obter essa informação chamando seu arquivo String.class.getClassLoader(). Para todas as classes de aplicação, o carregador "atual" geralmente é o do sistema.

Três princípios de carregamento de classe

  • Delegação

    A solicitação para carregar a classe é passada para o carregador pai, e uma tentativa de carregar a classe em si será feita somente se o carregador pai não conseguir localizar e carregar a classe. Essa abordagem permite carregar classes com o carregador o mais próximo possível do carregador base. Isso alcança visibilidade máxima da classe. Cada carregador mantém um registro das classes que foram carregadas por ele, colocando-as em seu cache. O conjunto dessas classes é chamado de escopo.

  • Visibilidade

    O carregador vê apenas “suas” classes e as classes do “pai” e não tem ideia das classes que foram carregadas por seu “filho”.

  • Singularidade

    Uma classe só pode ser carregada uma vez. O mecanismo de delegação garante que o carregador que inicia o carregamento da classe não sobrecarregue uma classe que foi previamente carregada na JVM.

Assim, ao escrever seu bootloader, um desenvolvedor deve ser guiado por estes três princípios.

Esquema de carregamento de classe

Quando ocorre uma chamada para carregar uma classe, esta classe é pesquisada no cache de classes já carregadas do carregador atual. Se a classe desejada não tiver sido carregada antes, o princípio da delegação transfere o controle para o carregador pai, que está localizado um nível acima na hierarquia. O carregador pai também tenta encontrar a classe desejada em seu cache. Se a classe já tiver sido carregada e o carregador souber sua localização, um objeto Classdessa classe será retornado. Caso contrário, a pesquisa continuará até chegar ao bootloader base. Se o carregador base não tiver informações sobre a classe necessária (ou seja, ainda não foi carregada), o bytecode desta classe será procurado no local das classes que o carregador determinado conhece, e se a classe não puder ser carregado, o controle retornará ao carregador filho, que tentará carregar a partir de fontes conhecidas por ele. Como mencionado acima, a localização das classes para o carregador base é a biblioteca rt.jar, para o carregador de extensão - o diretório com extensões jre/lib/ext, para o sistema - CLASSPATH, para o usuário pode ser algo diferente . Assim, o progresso do carregamento das classes segue na direção oposta - do carregador raiz para o atual. Quando o bytecode da classe é encontrado, a classe é carregada na JVM e uma instância do tipo é obtida Class. Como você pode ver facilmente, o esquema de carregamento descrito é semelhante à implementação do método acima loadClass(String name). Abaixo você pode ver este diagrama no diagrama.
Como as classes são carregadas na JVM – 2

Como uma conclusão

Nas primeiras etapas do aprendizado de uma linguagem, não há necessidade específica de entender como as classes são carregadas em Java, mas conhecer esses princípios básicos o ajudará a evitar o desespero ao encontrar erros como ClassNotFoundExceptionou NoClassDefFoundError. Bem, ou pelo menos entender aproximadamente qual é a raiz do problema. Assim, ocorre uma exceção ClassNotFoundExceptionquando uma classe é carregada dinamicamente durante a execução do programa, quando os carregadores não conseguem encontrar a classe necessária no cache ou ao longo do caminho da classe. Mas o erro NoClassDefFoundErroré mais crítico e ocorre quando a classe necessária estava disponível durante a compilação, mas não estava visível durante a execução do programa. Isso pode acontecer se o programa esquecer de incluir a biblioteca que utiliza. Pois bem, o próprio facto de compreender os princípios da estrutura da ferramenta que utiliza no seu trabalho (não necessariamente uma imersão clara e detalhada nas suas profundezas) acrescenta alguma clareza à compreensão dos processos que ocorrem dentro deste mecanismo, que, em por sua vez, leva ao uso confiante desta ferramenta.

Fontes

Como o ClassLoader funciona em Java No geral, uma fonte muito útil com uma apresentação acessível de informações. Carregando classes, ClassLoader Artigo bastante extenso, mas com ênfase em como fazer sua própria implementação de carregador com estes mesmos. ClassLoader: carregamento dinâmico de classes Infelizmente, este recurso não está disponível agora, mas lá encontrei o diagrama mais compreensível com um esquema de carregamento de classes, então não posso deixar de adicioná-lo. Especificação Java SE: Capítulo 5. Carregando, vinculando e inicializando
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION