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.
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
A relação dos carregadores de classes é a seguinte:
- O carregador de classes de inicialização, implementado por C++, não possui classe pai.
- Carregador de classes estendido (ExtClassLoader), implementado pela linguagem Java, o carregador de classes pai é nulo
- 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
- 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.
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.
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?
- Obtemos a classe de implementação específica "com.mysql.jdbc.Driver" do arquivo META-INF/services/java.sql.Driver
- 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?
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
- loaderClass: implementa a delegação parental
- findClass: usado para sobrescrever o carregamento, ou seja, de acordo com o nome da classe recebida, retornar o objeto Class correspondente
- 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:
- Substituir o método loadClass
- 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 é
- O nome totalmente qualificado é o mesmo
- 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
: 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 .
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