Mecanismo JVM - o tempo de carregamento da classe, o processo de carregamento da classe, delegação pai e a destruição do modelo de delegação pai

1. Visão Geral

A máquina virtual JVM carrega os dados que descrevem a classe do arquivo de classe para a memória e executa a verificação, conversão, análise e inicialização dos dados e, finalmente, forma um tipo Java que pode ser usado diretamente pela JVM. Esse é o mecanismo de carregamento de classe da JVM .

Na linguagem Java, o processo de carregamento da classe, conexão e inicialização são todos concluídos durante a execução do programa .

2. Tempo de carregamento da aula

Do momento em que a classe é carregada na memória até que a memória seja descarregada, o ciclo de vida da classe inclui 7 processos de carregamento , verificação , preparação , análise , inicialização , uso e descarregamento .

Mecanismo JVM - o tempo de carregamento da classe, o processo de carregamento da classe, delegação pai e a destruição do modelo de delegação pai

 

  1. Em que circunstâncias é necessário iniciar o primeiro estágio do processo de carregamento da classe: carregamento ? Não há restrições obrigatórias nesta especificação de máquina virtual Java e a implementação específica da máquina virtual pode ser controlada livremente.
  2. No entanto, para a fase de inicialização , a especificação da máquina virtual estipula estritamente que existem apenas cinco casos em que a classe deve ser " inicializada " imediatamente (carregamento, verificação e preparação são concluídos antes disso):

(1) Ao encontrar as instruções de 4 bytecode de new, getstatic, putstatic ou invokestatic , se a classe não foi inicializada, ela precisa acionar a inicialização primeiro. O cenário de código Java mais comum para gerar essas 4 instruções é: ao usar a nova palavra-chave para instanciar um objeto, ao ler ou definir uma variável estática de uma classe (modificada por final, o resultado foi colocado no pool constante em tempo de compilação Exceto para campos de constantes estáticas) e ao chamar um método estático de uma classe.

(2) Ao usar o método de pacote java.lang.reflect para fazer uma chamada reflexiva para uma classe, se a classe não foi inicializada, sua inicialização precisa ser disparada primeiro.

(3) Quando uma classe é inicializada, se for descoberto que o pai não foi inicializado, você precisa acionar a inicialização da classe pai.

(4) Quando a máquina virtual é iniciada, o usuário precisa especificar uma classe principal a ser executada (a classe que contém o método main ()) e a máquina virtual inicializa a classe principal primeiro.

(5) Ao usar o suporte de linguagem dinâmica de JDK1.7, se o resultado da análise final de uma instância java.lang.invoke.MethodHandle é o identificador de método de REF_getStatic, REF_putStatic, REF_invokeStatic e a classe correspondente a este identificador de método não foi inicializada , Você precisa acionar sua inicialização primeiro.

O comportamento dos cinco cenários acima é chamado de referência ativa a uma classe . Além disso, todas as outras maneiras de referenciar classes não acionarão a inicialização, que é chamada de referência passiva . Exemplos comuns de referências passivas incluem: (1) Referenciar as variáveis ​​estáticas da classe pai por meio da subclasse não fará com que a subclasse inicialize. System.out.println (SubClass.staticValue); (2) Fazer referência a uma classe por meio de uma definição de array não acionará a inicialização dessa classe. SuperClass [] arr = new SuperClass [10]; (3)  As constantes estáticas são armazenadas no pool de constantes da classe de chamada durante a fase de compilação e, em essência, não são diretamente referenciadas às constantes definidas. System.out.println (ConstClass.finalStaticValue);

3. O processo de carregamento da classe

Todo o processo de carregamento de classe JVM inclui: carregamento, verificação, preparação, análise e inicialização.

3.1 Carregando

Durante a fase de carregamento, a JVM precisa concluir as três coisas a seguir:

  1. Obtenha o fluxo de bytes binário que define essa classe por meio do nome totalmente qualificado de uma classe (como java.lang.String) .
  2. A estrutura de armazenamento estático representada por este fluxo de bytes é transformada na estrutura de dados de tempo de execução da área do método .
  3. Uma instância de objeto de java.lang.Class que representa essa classe é gerada na memória como a entrada de vários acessos a dados nesta classe na área do método .

3.2 Verificação

A verificação é a primeira etapa da fase de conexão. O objetivo desta fase é garantir que as informações contidas no fluxo de bytes do arquivo de classe atendam aos requisitos da máquina virtual atual e não ponham em risco a segurança da própria máquina virtual . No geral, a fase de verificação geralmente completará as seguintes quatro fases de ações de verificação: verificação de formato de arquivo, verificação de metadados, verificação de bytecode e verificação de referência de símbolo.

3.2.1 Verificação do formato do arquivo

O primeiro estágio é verificar se o fluxo de bytes está em conformidade com as especificações do formato de arquivo de classe e pode ser processado pela versão atual da máquina virtual.

3.2.2 Verificação de metadados

A segunda etapa é realizar a análise semântica das informações descritas pelo bytecode para garantir que as informações descritas atendam aos requisitos da especificação da linguagem Java. Os pontos de verificação que podem ser incluídos nesta etapa são os seguintes:

  1. Esta classe tem uma classe pai (exceto para java.lang.Object, todas as classes devem ter uma classe pai).
  2. Se a classe pai desta classe herda uma classe que não pode ser herdada (uma classe que é modificada final).
  3. Se esta classe não for uma classe abstrata, se ela implementou todos os métodos necessários para serem implementados em sua classe ou interface pai. e muitos mais

3.2.3 Verificação de Bytecode

O terceiro estágio é o mais complicado em todo o processo de verificação.O objetivo principal é determinar se a semântica do programa é legal e lógica por meio da análise do fluxo de dados e do fluxo de controle.

3.2.4 Verificação de referência de símbolo

O último estágio da verificação ocorre quando a máquina virtual converte as referências de símbolos em referências diretas.Esta ação de conversão ocorrerá no terceiro estágio da conexão: a fase de análise . A verificação de referência de símbolo pode ser vista como uma verificação de correspondência de informações além da própria classe ( várias referências de símbolo no conjunto de constantes  ) .

3.3 Preparação

A fase de preparação é a fase de alocar formalmente a memória para as variáveis ​​de classe (variáveis ​​de membro estáticas) e definir o valor inicial (valor zero) das variáveis ​​de classe.A memória utilizada por essas variáveis será alocada na área do método .

  1. A alocação de memória inclui apenas variáveis ​​de classe, não variáveis ​​de instância, que serão alocadas no heap junto com o objeto quando o objeto for instanciado.
  2. O valor inicial mencionado aqui é o valor zero do tipo de dados em "condições usuais". Suponha que uma variável de classe seja definida como:
public static int value=123;

Então, o valor do valor da variável após a fase de preparação é 0 em vez de 123. Porque desta vez ainda não foi iniciada qualquer método java, eo valor atribuído a 123 putstatic instrução após o programa é compilado, armazenado em um método construtor da classe () in, de modo que o valor atribuído à operação de 123 será a fase de inicialização vontade realizado.

  1. O "caso especial" é: quando o atributo de campo do campo de classe é ConstantValue (como uma constante estática), ele será inicializado com o valor especificado na fase de preparação, portanto, após marcar como final, o valor do valor é inicializado em 123 em vez de 0 na fase de preparação.
public static final intvalue =123;

3.4 Análise

O estágio de análise é uma máquina virtual, muitas vezes, quantidades pool de referências simbólicas substituídas por uma referência direta ao processo.

  1. Referências simbólicas: as referências simbólicas usam um conjunto de símbolos para descrever o alvo referenciado.O símbolo pode ser qualquer forma de literal, desde que possa ser usado para localizar o alvo sem ambigüidade . Referência de destino e pode não ter sido carregado na memória em.
  2. Referências diretas: uma referência direta pode ser um ponteiro que aponta diretamente para o alvo , um deslocamento relativo ou um identificador que pode localizar indiretamente o alvo . Se houver uma referência direta, o destino referenciado já deve existir na memória .

3.5 Inicialização

Na fase de inicialização, o código do programa Java (ou bytecode) definido na classe é realmente executado.

  1. A fase de inicialização é o processo de execução do método <clinit> () do construtor de classe. O método <clinit> () é gerado pelo compilador coletando automaticamente as ações de atribuição de todas as variáveis ​​de classe na classe e as instruções no bloco estático (bloco {} estático). A ordem em que o compilador coleta é determinada pela ordem em que as instruções aparecem no arquivo de origem. Em um bloco de instrução estática, apenas as variáveis ​​definidas antes do bloco de instrução estática podem ser acessadas. As variáveis ​​definidas depois dele podem ser usadas no bloco de instrução estática anterior. Atribuído, mas não pode ser acessado.
    public classTest{
        static { 
            i =0;// 给变量赋值可以正常编译通过System.out.print(i);// 这句编译器会提示“非法向前引用”
        }
        static int i =1;
    }
  1. O método <clinit> () é diferente do construtor de classe (ou método do construtor de instância <init> ()). Ele não precisa chamar explicitamente o construtor da classe pai, e a oportunidade virtual está garantida na subclasse <clinit> () Antes de o método ser executado, o método <clinit> () da classe pai foi executado. Portanto, a classe do primeiro método <clinit> () executado na máquina virtual deve ser java.lang.Object . Visto que o método <clinit> () da classe pai é executado primeiro, significa que o bloco de instrução estática definido na classe pai tem prioridade sobre a operação de atribuição de variável da subclasse. No código a seguir, o valor do campo B será 2 em vez de 1. .
    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }    }    static class Sub extends Parent {
        public static int B = A;
    }    public static void main (String[] args){
        System.out.println(Sub.B); // 输出结果是父类中的静态变量A的值,也就是2。
    }
  1. O método <clinit> () não é necessário para a classe ou interface. Se não houver nenhum bloco de instrução estática em uma classe e nenhuma operação de atribuição de variável, o compilador não poderá gerar o método <clinit> () para esta classe.
  2. Os blocos de instrução estática não podem ser usados ​​em interfaces, mas ainda existem operações de atribuição para inicialização de variável, portanto, interfaces e classes irão gerar métodos <clinit> (). Mas a diferença entre uma interface e uma classe é que o método <clinit> () da interface de implementação não precisa executar o método <clinit> () da interface pai primeiro. A interface pai será inicializada apenas quando as variáveis ​​definidas na interface pai forem usadas. Além disso, a classe de implementação da interface não executará o método <clinit> () da interface quando for inicializada.
  3. A máquina virtual garante que o método <clinit> () de uma classe está corretamente bloqueado e sincronizado em um ambiente multithread. Se vários threads inicializarem uma classe ao mesmo tempo, apenas um thread executará o <clinit> () dessa classe. ), Outras threads precisam bloquear e esperar até que a thread ativa termine de executar o método <clinit> (). Se houver uma operação muito demorada no método <clinit> () de uma classe, ela pode causar o bloqueio de vários processos [2] .Em aplicações práticas, esse bloqueio costuma estar muito oculto.

4. Modelo de delegação parental

4.1 Classes e carregadores de classes

  1. Carregador de classes : O módulo de código que implementa a ação de "obter um fluxo de bytes binário que descreve esta classe por meio do nome totalmente qualificado de uma classe" no estágio de carregamento de classes é chamado de "carregador de classes". A equipe de design da máquina virtual coloca essa ação fora da máquina virtual Java para implementar, para que o aplicativo sinta como obter as classes necessárias.
  2. Para qualquer classe, o carregador de classes que a carrega e a própria classe precisam estabelecer sua exclusividade na máquina virtual Java.Cada carregador de classes tem um espaço de nome de classe independente. Comparar se duas classes são "iguais" é significativo apenas se as duas classes são carregadas pelo mesmo carregador de classe, caso contrário, mesmo se as duas classes se originam do mesmo arquivo de classe e são carregadas pela mesma máquina virtual , Contanto que os carregadores de classes que os carregam sejam diferentes, as duas classes devem ser desiguais.

4.2 Tipos de carregadores de classe

Da perspectiva da máquina virtual Java, existem apenas dois carregadores de classes diferentes:

  1. Inicie o carregador de classes (Bootstrap ClassLoader), o carregador de classes usa a linguagem C ++, faz parte da própria máquina virtual .
  2. O outro são os carregadores de todas as outras classes.Esses carregadores de classes são implementados pela linguagem Java, independentemente da máquina virtual, e todos herdam da classe abstrata java.lang.ClassLoader .

Da perspectiva dos desenvolvedores Java, os carregadores de classes também podem ser divididos em três tipos de carregadores de classes fornecidos pelo sistema e carregadores de classes definidos pelo usuário.

  1. Bootstrap ClassLoader: Responsável por carregar e armazenar classes no diretório <JAVA_HOME> \ lib ou no caminho especificado pelo parâmetro -Xbootclasspath.
  2. Carregador de classes de extensão (Extension ClassLoader): Este carregador é implementado por sun.misc.Launcher $ ExtClassLoader, que é responsável por carregar o diretório <JAVA_HOME> \ lib \ ext ou as variáveis ​​de sistema são especificadas no caminho java.ext.dirs Para todas as bibliotecas de classes, os desenvolvedores podem usar diretamente o carregador de classes estendido.
  3. Carregador de classes de aplicativo (Application ClassLoader): Este carregador de classes é implementado por sun.misc.Launcher $ App-ClassLoader. Como esse carregador de classes é o valor de retorno do método getSystemClassLoader () em ClassLoader, geralmente é chamado de carregador de classes do sistema. É responsável por carregar a biblioteca de classes especificada no caminho de classe do usuário (ClassPath). Os desenvolvedores podem usar diretamente este carregador de classes. Se o aplicativo não personalizou seu próprio carregador de classes , geralmente esta é a classe padrão no programa Loader .
  4. Carregador de classes personalizado (User ClassLoader): carregador de classes definido pelo usuário. Quando os usuários escrevem seu próprio carregador de classes, se a solicitação de carregamento precisar ser delegada ao carregador de classes de bootstrap, apenas use null em vez disso. Para criar seu próprio carregador de classes, você só precisa herdar a classe java.lang.ClassLoader e, em seguida, substituir seu método findClass (String name), ou seja, especificar como obter o fluxo de bytecode da classe.
  • Se você deseja cumprir a especificação de delegação dos pais, reescreva o método findClass (lógica de carregamento de classe definida pelo usuário ).
  • Se você quiser destruí- lo, reescreva o método loadClass (a implementação lógica específica da delegação pai) .

O relacionamento entre esses carregadores de classes geralmente é mostrado na figura a seguir:

Mecanismo JVM - o tempo de carregamento da classe, o processo de carregamento da classe, delegação pai e a destruição do modelo de delegação pai

 

4.3 Modelo de delegação parental

Esse relacionamento hierárquico entre a figura mostra o carregador de classes, conhecido como modelo de delegação pai do carregador de classes (Modelo de Delegação de Pais).

  1. O modelo de delegação pai requer que, além do carregador de classes de inicialização de nível superior, todos os outros carregadores de classes devem ter seus próprios carregadores de classes pai.
  2. O modelo de delegação pai do carregador de classes foi introduzido no JDK 1.2. Quando não é um modelo de restrição obrigatório, é uma implementação do carregador de classes recomendado para desenvolvedores por designers Java.

O processo de trabalho do modelo de delegação pai é:

  1. Se um carregador de classes receber uma solicitação de carregamento de classe , ele não tentará primeiro carregar a própria classe, mas delegará a solicitação ao carregador de classes pai para concluir.
  2. Isso é verdadeiro para todos os níveis do carregador de classes, portanto, todas as solicitações de carregamento de classes devem ser transmitidas para o carregador de classes superior .
  3. Somente se o carregador pai relatar que não pode concluir a solicitação de carregamento, o carregador filho tentará carregá-lo sozinho .

O modelo de delegação pai pode ser usado para explicar uma pergunta: Por que você não pode personalizar a classe java.lang.String?

Resposta : Por meio do modelo de delegação pai, sabemos: Se o carregador de classe definido pelo usuário receber uma solicitação para carregar a classe java.lang.String e a classe java.lang.String não tiver sido carregada, o carregador de classe personalizado carregará a solicitação Delegado ao carregador pai, até que seja delegado ao carregador de classes de inicialização (Bootstrcp ClassLoader). O carregador de classe inicializável não reconhece a classe java.lang.String definida pelo usuário, ele carregará apenas a classe java.lang.String no JDK.

4.4 Destruir o modelo de delegação pai

Conforme mencionado acima, o modelo de delegação pai não é um modelo de restrição obrigatório , mas uma implementação de carregador de classes recomendada por designers Java para desenvolvedores. A maioria dos carregadores de classes no mundo Java segue esse modelo, mas há exceções. Até agora, o modelo de delegação dos pais experimentou principalmente 3 situações "quebradas" em grande escala.

  1. Para compatibilidade futura, java.lang.ClassLoader após JDK 1.2 adicionou um novo método protegido findClass (). Antes disso, o único propósito dos usuários herdarem java.lang.ClassLoader era substituir o método loadClass (), porque A máquina virtual chamará o método privado loadClassInternal () do carregador quando estiver carregando classes, e a única lógica desse método é chamar seu próprio loadClass ().
  2. A segunda destruição do modelo de delegação pai é devido aos defeitos do próprio modelo, que não podem resolver o problema de chamar o código do usuário para as classes básicas.

(1) É o serviço JNDI (Java Naming and Directory Interface) . O código do JNDI é carregado pelo carregador de classes de inicialização, mas o objetivo do JNDI é gerenciar e pesquisar recursos de maneira centralizada. Ele precisa ser implementado por fornecedores independentes e implantado no aplicativo. O código do provedor de interface JNDI em ClassPath, mas o carregador de classes de inicialização pode não reconhecer esses códigos. Portanto, a equipe de design Java apresentou: Thread Context ClassLoader. O serviço JNDI usa este carregador de classes de contexto de encadeamento para carregar o código SPI necessário, ou seja, o carregador de classes pai solicita que o carregador de classes filho conclua a ação de carregamento de classes, o que destrói o modelo de delegação pai.

(2) Por exemplo, tomcat , de acordo com a especificação Java Servlet, a própria prioridade da classe do aplicativo da Web deve ser maior do que a classe fornecida pelo contêiner da Web. Para o tomcat, para algumas classes não básicas descarregadas, o carregador de classe de cada aplicativo da web (WebAppClassLoader) será carregado primeiro e, quando houver falha no carregamento, ele será entregue ao commonClassLoader para seguir o modelo de delegação pai.

  1. A terceira "destruição" do modelo de delegação parental é causada pela busca do usuário pela natureza dinâmica do programa. A fim de obter hot swap, hot deployment e modularidade, significa adicionar uma função ou subtrair uma função sem reiniciar, apenas precisa substituir o módulo junto com o carregador de classes para realizar a substituição quente do código . Por exemplo, o surgimento da OSGi (Open Service Gateway Initiative). No ambiente OSGi, o carregador de classes não é mais uma estrutura de árvore no modelo de delegação pai, mas é desenvolvido em uma estrutura de rede.

Acho que você gosta

Origin blog.csdn.net/qq_45401061/article/details/108761241
Recomendado
Clasificación