Referência: https://www.jianshu.com/p/52c38cf2e3d4
Pontos principais:
- Princípios do mecanismo de carregamento de classes
- Ordem de inicialização de programas
- Modelo de proxy de carregamento de classes (mecanismo de delegação parental)
I. Mecanismo de Carregamento de Classes
O JVM carrega arquivos class na memória, realiza validação, preparação, análise e inicialização dos dados, formando um tipo Java que pode ser usado diretamente pelo JVM.
1. Carregamento
O carregamento transfere o arquivo de bytecode class para a memória, convertendo esses dados em dados de tempo de execução no método área (variáveis estáticas, blocos de código estático, pool de constantes, etc.), e gera um objeto Class no heap para representar esta classe (princípio de reflexão), servindo como ponto de acesso aos dados da classe no método área.
2. Linkagem
A linkagem mescla o código binário da classe Java ao estado de execução do JVM.
• ValidaçãoGarante que as informações da classe carregada estejam em conformidade com as especificações do JVM, sem problemas de segurança.
• PreparaçãoFase de alocação de memória real para variáveis de classe (variáveis estáticas) e configuração de valores iniciais. Essa memória será alocada no método área. Note que a configuração é com valores padrão, com atribuição específica concluída na fase de inicialização.
• AáliseProcesso de substituição de referências de símbolo no pool de constantes do virtual por referências diretas (referências de endereço).
3. Inicialização
A fase de inicialização é o processo de execução do método construtor de classe <clinit>(). O método construtor de classe <clinit>() é gerado automaticamente pelo compilador, combinando todas as ações de atribuição de variáveis de classe e instruções em blocos de código estático.
- Ao inicializar uma classe, se descobrir que sua classe pai não foi inicializada, é necessário inicializar primeiro a classe pai.
- A máquina virtual garante que o método <clinit>() de uma classe seja bloqueado e sincronizado corretamente em ambientes multithread.
II. Ordem de Inicialização de Programas Java
- Variáveis estáticas da classe pai
- Blocos de código estático da classe pai
- Variáveis estáticas da classe filha
- Blocos de código estático da classe filha
- Variáveis não estáticas da classe pai
- Blocos de código não estático da classe pai
- Método construtor da classe pai
- Variáveis não estáticas da classe filha
- Blocos de código não estático da clasce filha
- Método construtor da classe filha
/**
* @ClassName ExemploOrdemInicializacao
* @Description Teste da ordem de inicialização do programa
*/
public class ExemploOrdemInicializacao {
public static void main(String[] args) {
InstanciaB b = new InstanciaB();
}
}
class ClassePai {
static String variavelEstatica = "Variável estática da classe pai";
String variavelNaoEstatica = "Variável não estática da classe pai";
static {
System.out.println("Bloco de código estático da classe pai executado");
}
{
System.out.println("Bloco de código não estático da classe pai executado");
}
public ClassePai(){
System.out.println("Método construtor da classe pai executado");
}
}
class ClasseFilha extends ClassePai {
static String variavelEstatica = "Variável estática da classe filha";
String variavelNaoEstatica = "Variável não estática da classe filha";
static {
System.out.println("Bloco de código estático da classe filha executado");
}
{
System.out.println("Bloco de código não estático da classe filha executado");
}
public ClasseFilha(){
System.out.println("Método construtor da classe filha executado");
}
}
Saída do console:
Variável estática da classe pai
Bloco de código estático da classe pai executado
Variável estática da classe filha
Bloco de código estático da classe filha executado
Variável não estática da classe pai
Bloco de código não estático da classe pai executado
Método construtor da classe pai executado
Variável não estática da classe filha
Bloco de código não estático da classe filha executado
Método construtor da classe filha executado
III. Referências de Classes
1. Referência Ativa (sempre inicializará)
- Criar um objeto de uma classe.
- Chamar membros estáticos da classe (exceto constantes finais) e métodos estáticos.
- Usar métodos do pacote java.lang.reflect para reflexão da classe.
- Quando a máquina virtual inicia, java Hello, a classe Hello será sempre inicializada. Em outras palavras, a classe que contém o método main é inicializada primeiro.
- Ao inicializar uma classe, se sua classe pai não foi inicializada, a classe pai será inicializada primeiro.
2. Referência Passiva
- Ao acessar um domínio estático, apenas a classe que realmente declara este domínio será inicializada. Por exemplo: referenciar variáveis estáticas da classe pai através da classe filha não causará inicialização da classe filha.
- Definir referências de classe através de arrays não disparará a inicialização desta classe.
- Referenciar constantes não disparará a inicialização desta classe (as constantes são armazenadas no pool de constantes da classe de chamada durante a fase de compilação).
/**
* @ClassName ExemploReferenciaClasse
* @Description Teste de referências de classes
*/
public class ExemploReferenciaClasse {
public static void main(String[] args) throws ClassNotFoundException {
// Referência ativa: criar um objeto de classe
// Pessoa pessoa = new Pessoa();
// Referência ativa: chamar membros estáticos (exceto constantes finais) e métodos estáticos
// Pessoa.getIdade();
// System.out.println(Pessoa.idade);
// Referência ativa: usar métodos do pacote java.lang.reflect para reflexão
// Class.forName("pacote.exemplo.Pessoa");
// Referência passiva: ao acessar um domínio estático, apenas a classe que realmente declara este domínio será inicializada
// System.out.println(PessoaBranca.idade);
// Referência passiva: definir referências através de arrays não inicializará
// Pessoa[] pessoas = new Pessoa[10];
// Referência passiva: referenciar constantes não disparará a inicialização
System.out.println(Pessoa.numeroConstante);
}
}
class Pessoa {
static int idade = 3;
static final int numeroConstante = 20;
static {
System.out.println("Pessoa foi inicializada!");
}
public Pessoa() {
}
public static int getIdade() {
return idade;
}
public static void setIdade(int idade) {
Pessoa.idade = idade;
}
}
class PessoaBranca extends Pessoa {
static {
System.out.println("PessoaBranca foi inicializada!");
}
}
IV. Princípios dos Carregadores de Classes
1. Cache de Classes
Os carregadores de classes padrão do Java SE podem pesquisar classes conforme necessário. Uma vez que uma classe é carregada no carregador de classes, ela será mantida carregada (em cache) por algum tempo. No entanto, o coletor de lixo do JVM pode recuperar esses objetos Class.
2. Classificação dos Carregadores de Classes
Carregador de Classes Bootstrap (bootstrap class loader)(1) É usado para carregar as bibliotecas principais do Java (JAVA_HOME/jre/lib/rt.jar, conteúdo sob sun.boot.class.path). É implementado em código nativo (linguagem C) e não herda de java.lang.ClassLoader. (2) Carrega carregadores de classes de extensão e de aplicação, especificando seus carregadores de classes pai.
Carregador de Classes de Extensão (extensions class loader)(1) É usado para carregar bibliotecas de extensão do Java (JAVA_HOME/jre/ext/*.jar, ou conteúdo sob java.ext.dirs). A implementação da máquina virtual Java fornece um diretório de biblioteca de extensão. Este carregador de classes pesquisa e carrega classes Java neste diretório. (2) Implementado por sun.misc.Launcher$ExtClassLoader.
Carregador de Classes de Aplicação (application class loader)(1) Ele carrega classes Java com base no caminho de classe da aplicação Java (classpath, conteúdo sob java.class.path). Geralmente, as classes de uma aplicação Java são carregadas por este carregador. (2) Implementado por sun.misc.Launcher$AppClassLoader.
Carregador de Classes Personalizado(1) Desenvolvedores podem implementar seus próprios carreegadores de classes herdando a classe java.lang.ClassLoader para atender a necessidades especiais.
3. Classe java.lang.ClassLoader
(1) Função:
- A responsabilidade básica da classe java.lang.ClassLoader é, com base em um nome de classe especificado, encontrar ou gerar seu bytecode correspondente, e então definir uma classe Java a partir desses bytecodes, ou seja, uma instância da classe java.lang.Class.
- O ClassLoader também é responsável por carregar recursos necessários para aplicativos Java, como arquivos de imagem e arquivos de configuração.
(2) Métodos comuns:
- getParent() Retorna o carregador de classes pai deste carregador de classes.
- loadClass(String name) Carrega uma classe com o nome name, retornando uma instância da classe java.lang.Class.
- findClass(String name) Procura uma classe com o nome name, retornando uma instância da classe java.lang.Class.
- findLoadedClass(String name) Procura uma classe com o nome name que já foi carregada, retornando uma instância da classe java.lang.Class.
- defineClass(String name, byte[] b, int off, int len) Converte o conteúdo do array de bytes b em uma classe Java, retornando uma instância da classe java.lang.Class. Este método é declarado como final.
- resolveClass(Class<?> c) Liga a classe Java especificada.
V. Modelo de Proxy de Carregadores de Classes
O modelo de proxy delega o carregamento de classes especificadas para outros carregadores de classes. O mecanismo de delegação parental é comumente usado.
1. Mecanismo de Delegação Parental
Quando um carregador de classes específico recebe uma solicitação de carregamento de classe, ele delega a tarefa de carregamento para seu carregador de classes pai, até o carregador de classes pai mais alto, o carregador de classes bootstrap. Se o carregador de classes pai puder carregar, ele carrega; caso contrário, retorna ao carregador de classes filho para carregar. Se nenhum puder carregar, um erro é reportado: ClassNotFoundException.
O mecanismo de delegação parental garante a segurança de tipo das bibliotecas principais do Java. Este mecanismo garante que os usuários não possam definir classes como java.lang.Object. Por exemplo, se um usuário define java.lang.String, ao carregar esta classe, o carregador de classes mais alto tentará carregar primeiro, encontrando esta classe também nas classes principais, carregando assim a biblioteca de classes principais, enquanto a definição personalizada nunca será carregada.
É notável que o mecanismo de delegação parental é um tipo de modelo de proxy, mas nem todos os carregadores de classes usam este mecanismo. No servidor Tomcat, o carregador de classes também usa um modelo de proxy, diferindo-se por tentar primeiro carregar uma classe, e se não encontrar, delegar ao carregador de classes pai. Esta ordem é oposta à dos carregadores de classes padrão.
VI. Carregadores de Classes Personalizados
Fluxo do Carregador de Classes Personalizado
(1) Primeiro, verifique se o tipo solicitado já foi carregado no namespace deste carregador de classes. Se já foi carregado, retorne diretamente; caso contrário, prossiga para o passo 2. (2) Delegue a solicitação de carregamento de classe ao carregador de classes pai. Se o carregador de classes pai puder concluir, retorne a instância Class carregada pelo carregador de classes pai; caso contrário, prossiga para o passo 3. (3) Chame o método findClass(...) deste carregador de classes, tentando obter o bytecode correspondente. Se obtiver, chame defineClass(...) para importar o tipo para o método área; se não obtiver o bytecode ou falhar por outras razões, retorne uma exceção para loadClass(...), que lança a exceção e termina o processo de carregamento (note: aqui existem vários tipos de exceções).
- Nota: Classes carregadas por dois carregadores de classes diferentes são consideradas classes diferentes pelo JVM.
import java.io.*;
/**
* @ClassName CarregadorArquivoSistema
* @Description Carregador de classes de arquivo personalizado
*/
public class CarregadorArquivoSistema extends ClassLoader {
private String diretorioRaiz; // Diretório raiz
public CarregadorArquivoSistema(String diretorioRaiz) {
this.diretorioRaiz = diretorioRaiz;
}
@Override
protected Class<?> findClass(String nome) throws ClassNotFoundException {
Class<?> classeCarregada = findLoadedClass(nome); // Verifica se a classe já foi carregada
if(classeCarregada != null){ // A classe já foi carregada, retorna diretamente
return classeCarregada;
} else { // A classe ainda não foi carregada
ClassLoader carregadorPai = this.getParent(); // Delega ao carregador pai
try {
classeCarregada = carregadorPai.loadClass(nome);
} catch (ClassNotFoundException e) {
// Não tratado intencionalmente
}
if(classeCarregada != null){ // Carregamento pelo pai bem-sucedido, retorna
return classeCarregada;
} else {
byte[] dadosClasse = getDadosClasse(nome);
if(dadosClasse == null){
throw new ClassNotFoundException();
} else {
classeCarregada = defineClass(nome, dadosClasse, 0, dadosClasse.length);
}
}
}
return classeCarregada;
}
private byte[] getDadosClasse(String nome) {
// pacote.classe.Exemplo --> D:/meuprojeto/pacote/classe/Exemplo.class
String caminho = diretorioRaiz + "/" + nome.replace('.', '/') + ".class";
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(caminho);
byte[] buffer = new byte[1024];
int bytesLidos = 0;
while((bytesLidos = is.read(buffer)) != -1){
baos.write(buffer, 0, bytesLidos);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
try {
if(baos != null){
baos.close();
}
if(is != null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
VII. Carregador de Classes de Contexto de Thread
Normalmente, quando você precisa carregar dinamicamente recursos, você tem pelo menos três ClassLoader para escolher:
- Carregador de sistema ou carregador de aplicação (system classloader or application classloader)
- Carregador de classe atual
- Carregador de classe do thread atual
• O carregador de classe do thread atual é para abandonar o modo de cadeia de carregamento de delegação parental. Cada thread tem um carregador de contexto associado. Se você usa new Thread() para gerar uma nova thread, a nova thread herdará o carregador de contexto de seu thread pai. Se o programa não fizer nenhuma alteração no carregador de contexto do thread, todos os threads no programa usarão o carregador de sistema como carregador de contexto. • Thread.currentThread().getContextClassLoader()
VIII. Carregador de Classes do Servidor Tomcat
Cada aplicação Web tem uma instância correspondente de carregador de classes. Este carregador de classes também usa um modelo de proxy (diferente do mecanismo de delegação parental mencionado anteriormente), diferindo-se por tentar primeiro carregar uma classe, e se não encontrar, delegar ao carregador de classes pai. Esta ordem é oposta à dos carregadores de classes padrão. Mas também é para garantir a segurança, assim as bibliotecas principais não estão no escopo de consulta.