Combate JVM: o princípio e aplicação do ClassLoader

insira a descrição da imagem aqui

prefácio

Me fizeram uma pergunta como esta durante a entrevista: Se você construir uma classe java.lang.String em seu projeto, a classe String usada no sistema é a classe String que você definiu ou a classe String na API nativa?

Você pode tentar e descobrir que a classe String na API nativa ainda é usada no sistema final.Por que isso acontece? Isso tem que começar com o processo de carregamento de classe.
insira a descrição da imagem aqui

Todos nós sabemos que Java é multiplataforma porque JVMs em diferentes plataformas podem interpretar arquivos bytecode como instruções de máquina local.Como a JVM carrega arquivos bytecode? A resposta é ClassLoader, vamos imprimir o objeto ClassLoader primeiro

public class ClassLoaderDemo1 {
    
    

    public static void main(String[] args) {
    
    
        // null
        System.out.println(String.class.getClassLoader());
        ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
        while (loader != null) {
    
    
            // sun.misc.Launcher$AppClassLoader@58644d46
            // sun.misc.Launcher$ExtClassLoader@7ea987ac
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

Modelo de delegação dos pais

Para entender essa saída, precisamos falar sobre o modelo de delegação pai. Se um carregador de classes receber uma solicitação de carregamento de classe, ele não a carregará primeiro, mas delegará a solicitação ao carregador de classes pai para executar. If Se o carregador de classes pai também tiver seu carregador de classes pai, ele será delegado para cima, recursivamente, e a solicitação eventualmente alcançará o carregador de classes de inicialização de nível superior. Se o carregador de classes pai puder concluir a tarefa de carregamento de classe, ele retornará com êxito. Se a classe pai está carregada Se o carregador não puder completar a tarefa de carregamento, o subcarregador tentará carregá-la sozinho. Este é o modo de delegação parental , ou seja, todo filho é preguiçoso, e toda vez que tiver trabalho, ele sairá que o pai o faça.Quando não pode ser feito, o filho encontra uma maneira de completá-lo sozinho. O relacionamento pai-filho no modelo de delegação pai não é o chamado relacionamento de herança de classe, mas um relacionamento de combinação para reutilizar o código relevante do carregador de classes pai .

A vantagem de usar o modo de delegação pai é que a classe Java possui um relacionamento hierárquico com prioridade junto com seu carregador de classes, através do qual pode ser evitado , quando o pai já carregou a classe, não há necessidade para carregar o ClassLoader filho novamente.

Em segundo lugar, considerando os fatores de segurança, os tipos definidos na API do núcleo java não serão substituídos arbitrariamente . Suponha que uma classe chamada java.lang.Integer seja passada pela rede, passada para o carregador de classes de inicialização por meio do modo de delegação pai e a inicialização carregador de classes A classe com este nome é encontrada no núcleo da API Java, e verifica-se que a classe foi carregada, e o java.lang.Integer passado pela rede não será recarregado, mas o Integer.class carregado será retornado diretamente, o que pode impedir que as principais bibliotecas da API sejam adulteradas à vontade.

O processo de verificação e carregamento e o papel do ClassLoader fornecido pelo sistema são mostrados na figura abaixo. O problema no início do artigo, é fácil de entender usando o carregamento pai para explicar a classe String na api nativa
insira a descrição da imagem aqui

A relação dos carregadores de classes é a seguinte:

  1. O carregador de classes de inicialização, implementado por C++, não possui classe pai.
  2. Carregador de classes estendido (ExtClassLoader), implementado pela linguagem Java, o carregador de classes pai é nulo
  3. Carregador de classes do aplicativo (AppClassLoader), implementado pela linguagem Java, carregador de classes pai é ExtClassLoader carregador de classes do sistema (AppClassLoader), implementado pela linguagem Java, carregador de classes pai é ExtClassLoader
  4. Carregador de classes definido pelo usuário, o carregador de classes pai deve ser AppClassLoader. Carregador de classes personalizado, o carregador de classes pai deve ser AppClassLoader.

insira a descrição da imagem aqui

Quebrando o modelo de delegação dos pais

No entanto, devido à limitação do escopo de carregamento, o ClassLoader de nível superior não pode acessar as classes carregadas pelo ClassLoader de nível inferior. Portanto, é necessário destruir o modelo de delegação pai neste momento . Tomando JDBC como exemplo, falarei sobre porque o modelo de delegação pai deve ser destruído.

Não usando Java SPI

Quando começamos a operar o banco de dados com JDBC, você deve ter escrito o seguinte código. Primeiro carregue a classe de implementação do driver e, em seguida, obtenha o link do banco de dados por meio do DriverManager

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    
    
    public Driver() throws SQLException {
    
    
    }

    static {
    
    
        try {
    
    
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
    
    
            throw new RuntimeException("Can't register driver!");
        }
    }
}

Registre a classe de implementação de driver correspondente com DriverManager na classe Driver

Usando Java SPI

Após o JDBC 4.0, é suportado o uso de SPI para registrar o driver, o método específico é indicar qual driver está sendo usado atualmente no arquivo META-INF/services/java.sql.Driver no pacote jar do mysql.

SPI é o modo de estratégia. A classe de implementação da interface de tempo de execução é determinada de acordo com a configuração.
insira a descrição da imagem aqui
Ao usar drivers diferentes, não precisamos carregar manualmente a classe do driver através de Class.forName, basta importar o pacote jar correspondente . Assim, o código acima pode ser alterado para o seguinte formulário

Connection conn = DriverManager.getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");

Então, quando a classe de driver correspondente é carregada?

  1. Obtemos a classe de implementação específica "com.mysql.jdbc.Driver" do arquivo META-INF/services/java.sql.Driver
  2. Carregue esta classe através de Class.forName("com.mysql.jdbc.Driver")

DriverManager está no pacote rt.jar, então DriverManager é carregado através do carregador de classes de inicialização. E Class.forName() é carregado com o ClassLoader do chamador, então se você usar o carregador de classes de inicialização para carregar com.mysql.jdbc.Driver, ele definitivamente não será carregado ( porque em geral, o carregador de classes de inicialização carrega apenas o rt pacote .jar na classe ha ).

Como resolvê-lo?

Se você quiser que o ClassLoader de nível superior carregue o ClassLoader de nível inferior, você só pode destruir o mecanismo de delegação pai. Vamos ver como DriverManager faz isso

Quando o DriverManager é carregado, o bloco de código estático é executado e, no bloco de código estático, o método loadInitialDrivers é executado.
E este método carregará a classe de driver correspondente.

public class DriverManager {
    
    

    static {
    
    
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
    
    

        // 省略部分代码
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
    
            public Void run() {
    
    

                // 根据配置文件加载驱动实现类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
    
    
                    while(driversIterator.hasNext()) {
    
    
                        driversIterator.next();
                    }
                } catch(Throwable t) {
    
    
                // Do nothing
                }
                return null;
            }
        });

        // 省略部分代码
    }

}

Vamos ver que tipo de ClassLoader ele está usando, podemos ver que o carregador de contexto de thread é obtido executando Thread.currentThread().getContextClassLoader().

O carregador de classes de contexto de encadeamento pode ser definido pelo método Thread.setContextClassLoader(), o padrão é o carregador de classes do aplicativo (AppClassLoader)

// ServiceLoader#load
public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

Tanto ExtClassLoader quanto AppClassLoader são criados através da classe Launcher.No construtor da classe Launcher, podemos ver que o carregador de classes de contexto de thread é AppClassLoader por padrão.

public class Launcher {
    
    

    public Launcher() {
    
    
        Launcher.ExtClassLoader var1;
        try {
    
    
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
    
    
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
    
    
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
    
    
            throw new InternalError("Could not create application class loader", var9);
        }

        // 设置线程上下文类加载器为AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);

        // 省略部分代码

    }
}    

Obviamente, o carregador de classes de contexto de thread permite que o carregador de classes pai carregue a classe chamando o carregador de classes filho, o que quebra o princípio do modelo de delegação pai

carregador de classes personalizado

Por que carregador de classe personalizado?

insira a descrição da imagem aqui

Implementação do ClassLoader

Java fornece a classe abstrata java.lang.ClassLoader, todos os carregadores de classe definidos pelo usuário devem herdar a classe ClassLoader , então vamos dar uma olhada na lógica de ClassLoader primeiro, e dar uma olhada na lógica principal de seu carregamento de classe

Eu interceptei 3 métodos importantes

  1. loaderClass: implementa a delegação parental
  2. findClass: usado para sobrescrever o carregamento, ou seja, de acordo com o nome da classe recebida, retornar o objeto Class correspondente
  3. defineClass: método local, a classe final carregada só pode ser feita através de defineClass
// 从这方法开始加载
public Class<?> loadClass(String name) throws ClassNotFoundException {
    
    
	return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
    
    
	synchronized (getClassLoadingLock(name)) {
    
    
		// First, check if the class has already been loaded
		// 先从缓存查找该class对象,找到就不用重新加载
		Class<?> c = findLoadedClass(name);
		if (c == null) {
    
    
			long t0 = System.nanoTime();
			try {
    
    
				if (parent != null) {
    
    
                    // 如果加载不到,委托父类去加载	
                    // 这里体现了自底向上检查类是否已经加载					
					c = parent.loadClass(name, false);
				} else {
    
    
				    // 如果没有父类,委托启动加载器去加载
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
    
    
				// ClassNotFoundException thrown if class not found
				// 这里体现了自顶向下尝试加载类,当父类加载加载不到时
				// 会抛出ClassNotFoundException
				// from the non-null parent class loader
			}

			if (c == null) {
    
    
				// If still not found, then invoke findClass in order
				// to find the class.
				long t1 = System.nanoTime();
				// 如果都没有找到,通过自己的实现的findClass去加载
				// findClass方法没有找到会抛出ClassNotFoundException
				c = findClass(name);

				// this is the defining class loader; record the stats
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
				sun.misc.PerfCounter.getFindClasses().increment();
			}
		}
		// 是否需要在加载时进行解析
		if (resolve) {
    
    
			resolveClass(c);
		}
		return c;
	}
}

findClass é usado para sobrescrever o carregamento

protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
	throw new ClassNotFoundException(name);
}

Como personalizar o carregador de classes?

Java fornece a classe abstrata java.lang.ClassLoader, todos os carregadores de classes definidos pelo usuário devem herdar a classe ClassLoader

Ao customizar o carregador de classes, há duas práticas comuns:

  1. Substituir o método loadClass
  2. Substituir o método findClass

Mas, em geral, você pode reescrever o método findClass, não o método loadClass. Como loadClass é usado para implementar o modelo de delegação pai, modificar esse método fará com que o modelo seja destruído, o que é fácil de operar. Portanto, geralmente reescrevemos o método findClass e retornamos o objeto Class correspondente de acordo com o nome da classe recebida

Em seguida, escreveremos um ClassLoader por nós mesmos para carregar o arquivo de classe do arquivo especificado

public class DemoObj {
    
    

    public String toString() {
    
    
        return "I am DemoObj";
    }

}

javac gera o arquivo de classe correspondente, o coloca no diretório especificado e o carrega por FileClassLoader

public class FileClassLoader extends ClassLoader {
    
    

    // class文件的目录
    private String rootDir;

    public FileClassLoader(String rootDir) {
    
    
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        byte[] classData = getClassData(name);
        if (classData == null) {
    
    
            throw new ClassNotFoundException();
        } else {
    
    
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
    
    

        String path = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
    
    
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
    
    
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
    
    

        String rootDir = "/Users/peng/study-code/java-learning/src/main/java";
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
    
    
            // 传入class文件的全限定名
            Class<?> clazz = loader.loadClass("com.javashitang.classloader.DemoObj");
            // com.javashitang.classloader.FileClassLoader@1b28cdfa
            System.out.println(clazz.getClassLoader());
            // I am DemoObj
            System.out.println(clazz.newInstance().toString());
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

Aplicação de carregador personalizado

Ele pode criptografar e revelar arquivos de classe, realizar a implantação a quente de aplicativos e realizar o isolamento de aplicativos. Tome o ClassLoader no Tomcat como exemplo para demonstrar

Antes de explicar o papel de evitar a duplicação de classes, uma pergunta é lançada, a identificação exclusiva de um objeto Class pode ser determinada apenas pelo nome totalmente qualificado?

A resposta é não, porque você não pode garantir que classes com o mesmo nome totalmente qualificado não apareçam em vários projetos

A condição para a JVM julgar se duas classes são iguais é

  1. O nome totalmente qualificado é o mesmo
  2. carregado pelo mesmo carregador de classes

Vamos verificá-lo com o FileClassLoader escrito acima

String rootDir = "/Users/peng/study-code/java-learning/src/main/java";
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);

Class class1 = loader1.findClass("com.javashitang.classloader.DemoObj");
Class class2 = loader2.findClass("com.javashitang.classloader.DemoObj");

// false
System.out.println(class1 == class2);

A distribuição da área de dados de tempo de execução é a seguinte
insira a descrição da imagem aqui
: Muitos ClassLoaders são definidos no Tomcat para obter o isolamento do aplicativo.

Um ClassLoader comum é fornecido no Tomcat, que é o principal responsável por carregar classes e pacotes Jar usados ​​pelo Tomcat, bem como algumas classes e pacotes Jar comuns a aplicativos, como todas as classes e pacotes Jar no diretório CATALINA_HOME/lib.

O Tomcat irá criar um carregador de classes exclusivo para cada aplicação implementada, ou seja, WebApp ClassLoader, que é responsável por carregar os arquivos Jar no diretório WEB-INF/lib da aplicação e os arquivos Class no diretório WEB-INF/classes. Como cada aplicativo tem seu próprio WebApp ClassLoader, diferentes aplicativos da Web podem ser isolados uns dos outros e não podem ver os arquivos de classe usados ​​uns pelos outros. Funciona mesmo que os nomes totalmente qualificados das classes em projetos diferentes possam ser iguais .

insira a descrição da imagem aqui

Quando o aplicativo for implementado a quente, o WebApp ClassLoader original será descartado e um novo WebApp ClassLoader será criado para o aplicativo.

Blogue de referência

JDBC destrói o modelo de delegação pai
[0] https://www.jianshu.com/p/09f73af48a98
Você pode entender de relance, a explicação detalhada do ClassLoader em java super detalhado
[1] https://blog.csdn. net/briblue/article /details/54973413
Compreensão aprofundada do Java ClassLoader (ClassLoader)
[2]https://blog.csdn.net/javazejian/article/details/73413292Análise
aprofundada do mecanismo do Java ClassLoader (nível de origem)
[3]http://www.hollischuang.com/archives/199
Análise detalhada e aprofundada do mecanismo de funcionamento do Java ClassLoader
[4]https://segmentfault.com/a/1190000008491597Análise
detalhada do princípio do Java ClassLoader
[5]https://blog.csdn.net/xyang81/article/details/7292380
Compreensão aprofundada do ClassLoader na JVM
[6] https://juejin.im/post/5a27604d51882503dc53938d

Acho que você gosta

Origin blog.csdn.net/zzti_erlie/article/details/123379622
Recomendado
Clasificación