Combate JVM: el principio y la aplicación de ClassLoader

inserte la descripción de la imagen aquí

prefacio

Me hicieron una pregunta como esta durante la entrevista: si crea una clase java.lang.String en su proyecto, ¿la clase String utilizada en el sistema es la clase String que definió o la clase String en la API nativa?

Puede probarlo y descubrir que la clase String en la API nativa todavía se usa en el sistema final. ¿Por qué sucede esto? Esto tiene que comenzar con el proceso de carga de clases.
inserte la descripción de la imagen aquí

Todos sabemos que Java es multiplataforma porque las JVM en diferentes plataformas pueden interpretar los archivos de bytecode como instrucciones de la máquina local.¿Cómo carga JVM los archivos de bytecode? La respuesta es ClassLoader, primero imprimamos el objeto ClassLoader

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 delegación de padres

Para comprender este resultado, tenemos que hablar sobre el modelo de delegación principal. Si un cargador de clases recibe una solicitud de carga de clases, no la cargará primero, sino que delegará la solicitud al cargador de clases principal para que la ejecute. Si el cargador de clases principal también tiene su cargador de clases principal, se delegará hacia arriba, recursivamente a su vez, y la solicitud finalmente llegará al cargador de clases de inicio de nivel superior. Si el cargador de clases principal puede completar la tarea de carga de clases, regresará con éxito. Si la clase principal está cargada Si el cargador no puede completar la tarea de carga, el subcargador intentará cargarlo por sí mismo. Este es el modo de delegación de padres , es decir, cada hijo es perezoso y cada vez que tiene trabajo, se irá. le pide a su padre que lo haga, cuando no se puede hacer, el hijo encuentra la manera de completarlo por sí mismo. La relación padre-hijo en el modelo de delegación padre no es la llamada relación de herencia de clases, sino una relación de combinación para reutilizar el código relevante del cargador de clases padre .

La ventaja de usar el modo de delegación principal es que la clase Java tiene una relación jerárquica con prioridad junto con su cargador de clases, a través de la cual se puede evitar , cuando el padre ya ha cargado la clase, no hay necesidad. para cargar de nuevo el ClassLoader hijo.

En segundo lugar, teniendo en cuenta los factores de seguridad, los tipos definidos en la API principal de Java no se reemplazarán arbitrariamente Supongamos que una clase llamada java.lang.Integer se pasa a través de la red, se pasa al cargador de clases de inicio a través del modo de delegación principal y el inicio cargador de clases La clase con este nombre se encuentra en la API central de Java, y se encuentra que la clase ha sido cargada, y el java.lang.Integer pasado por la red no se volverá a cargar, pero el Integer.class cargado será devueltos directamente, lo que puede evitar que las bibliotecas API principales se manipulen a voluntad.

El proceso de verificación y carga y el rol del ClassLoader provisto por el sistema se muestran en la figura a continuación. El problema al principio del artículo, es fácil de entender utilizando la carga principal para explicar la clase String en la API nativa.
inserte la descripción de la imagen aquí

La relación de los cargadores de clases es la siguiente:

  1. El cargador de clases de inicio, implementado por C++, no tiene una clase principal.
  2. Cargador de clases extendido (ExtClassLoader), implementado por el lenguaje Java, el cargador de clases principal es nulo
  3. Cargador de clases de aplicaciones (AppClassLoader), implementado por el lenguaje Java, el cargador de clases padre es ExtClassLoader cargador de clases del sistema (AppClassLoader), implementado por el lenguaje Java, el cargador de clases padre es ExtClassLoader
  4. Cargador de clases definido por el usuario, el cargador de clases principal debe ser AppClassLoader. Cargador de clases personalizado, el cargador de clases principal debe ser AppClassLoader.

inserte la descripción de la imagen aquí

Rompiendo el modelo de delegación parental

Sin embargo, debido a la limitación del ámbito de carga, el ClassLoader de nivel superior no puede acceder a las clases cargadas por el ClassLoader de nivel inferior. Por lo tanto, es necesario destruir el modelo de delegación principal en este momento Tomando JDBC como ejemplo, hablaré sobre por qué se debe destruir el modelo de delegación principal.

No usar Java SPI

Cuando comenzamos a operar la base de datos con JDBC, debe haber escrito el siguiente código. Primero cargue la clase de implementación del controlador y luego obtenga el enlace de la base de datos a través de 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 la clase de implementación del controlador correspondiente con DriverManager en la clase Driver

Uso de Java SPI

Después de JDBC 4.0, se admite el uso de SPI para registrar el controlador. El método específico es indicar qué controlador se usa actualmente en el archivo META-INF/services/java.sql.Driver en el paquete jar de mysql.

SPI es el modo de estrategia. La clase de implementación de la interfaz de tiempo de ejecución se determina de acuerdo con la configuración.
inserte la descripción de la imagen aquí
Cuando usamos diferentes controladores, no necesitamos cargar manualmente la clase de controlador a través de Class.forName, solo necesitamos importar el paquete jar correspondiente . Entonces, el código anterior se puede cambiar a la siguiente forma

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

Entonces, ¿cuándo se carga la clase de controlador correspondiente?

  1. Obtenemos la clase de implementación específica "com.mysql.jdbc.Driver" del archivo META-INF/services/java.sql.Driver
  2. Cargue esta clase a través de Class.forName("com.mysql.jdbc.Driver")

DriverManager está en el paquete rt.jar, por lo que DriverManager se carga a través del cargador de clases de inicio. Y Class.forName() se carga con el ClassLoader de la persona que llama, por lo que si usa el cargador de clases de inicio para cargar com.mysql.jdbc.Driver, definitivamente no se cargará ( porque, en general, el cargador de clases de inicio solo carga el rt paquete .jar en la clase ha ).

¿Cómo resolverlo?

Si desea que el ClassLoader de nivel superior cargue el ClassLoader de nivel inferior, solo puede destruir el mecanismo de delegación principal. Veamos cómo lo hace DriverManager

Cuando se carga DriverManager, se ejecuta el bloque de código estático y, en el bloque de código estático, se ejecuta el método loadInitialDrivers.
Y este método cargará la clase de controlador correspondiente.

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;
            }
        });

        // 省略部分代码
    }

}

Veamos qué tipo de ClassLoader está usando, podemos ver que el cargador de contexto de subprocesos se obtiene ejecutando Thread.currentThread().getContextClassLoader().

El cargador de clases de contexto de subprocesos se puede configurar mediante el método Thread.setContextClassLoader(), el predeterminado es el cargador de clases de aplicación (AppClassLoader)

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

Tanto ExtClassLoader como AppClassLoader se crean a través de la clase Launcher.En el constructor de la clase Launcher, podemos ver que el cargador de clases de contexto del subproceso es AppClassLoader por defecto.

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, el cargador de clases de contexto de subprocesos permite que el cargador de clases principal cargue la clase llamando al cargador de clases secundario, lo que rompe el principio del modelo de delegación principal.

cargador de clases personalizado

¿Por qué un cargador de clases personalizado?

inserte la descripción de la imagen aquí

Implementación del cargador de clases

Java proporciona la clase abstracta java.lang.ClassLoader, todos los cargadores de clases definidos por el usuario deben heredar la clase ClassLoader , así que echemos un vistazo a la lógica de ClassLoader primero y echemos un vistazo a la lógica principal de su carga de clases.

Intercepté 3 métodos importantes

  1. loaderClass: implementa la delegación de padres
  2. findClass: se usa para sobrescribir la carga, es decir, de acuerdo con el nombre de la clase entrante, devuelve el objeto Class correspondiente
  3. defineClass: método local, la clase cargada final solo se puede hacer a travé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 se utiliza para sobrescribir la carga

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

¿Cómo personalizar el cargador de clases?

Java proporciona la clase abstracta java.lang.ClassLoader, todos los cargadores de clases definidos por el usuario deben heredar la clase ClassLoader

Al personalizar el cargador de clases, hay dos prácticas comunes de la siguiente manera:

  1. Anular el método loadClass
  2. Anular el método findClass

Pero, en general, puede reescribir el método findClass, no el método loadClass. Debido a que loadClass se usa para implementar el modelo de delegación principal, la modificación de este método hará que el modelo se destruya, lo cual es fácil de operar. Por lo tanto, generalmente reescribimos el método findClass y devolvemos el objeto Class correspondiente de acuerdo con el nombre de la clase entrante.

A continuación, escribiremos un ClassLoader nosotros mismos para cargar el archivo de clase desde el archivo especificado

public class DemoObj {
    
    

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

}

javac genera el archivo de clase correspondiente, lo coloca en el directorio especificado y luego lo carga mediante 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();
        }
    }
}

Aplicación de cargador personalizado

Puede cifrar y revelar archivos de clase, implementar aplicaciones en caliente y aislar aplicaciones. Tome ClassLoader en Tomcat como ejemplo para demostrar

Antes de explicar el papel de prevenir la duplicación de clases, se lanza una pregunta: ¿la identificación única de un objeto de clase puede determinarse solo por el nombre completo?

La respuesta es no, porque no puede garantizar que las clases con el mismo nombre completo no aparecerán en varios proyectos.

La condición para que JVM juzgue si dos clases son iguales es

  1. El nombre completo es el mismo
  2. cargado por el mismo cargador de clases

Verifiquémoslo con el FileClassLoader escrito arriba

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);

La distribución del área de datos de tiempo de ejecución es la siguiente
inserte la descripción de la imagen aquí
: Muchos ClassLoaders se definen en Tomcat para lograr el aislamiento de la aplicación.

Se proporciona un Common ClassLoader en Tomcat, que es principalmente responsable de cargar clases y paquetes Jar utilizados por Tomcat, así como algunas clases y paquetes Jar comunes a las aplicaciones, como todas las clases y paquetes Jar en el directorio CATALINA_HOME/lib.

Tomcat creará un cargador de clases único para cada aplicación implementada, es decir, WebApp ClassLoader, que se encarga de cargar los archivos Jar en el directorio WEB-INF/lib de la aplicación y los archivos Class en el directorio WEB-INF/classes. Dado que cada aplicación tiene su propio WebApp ClassLoader, las diferentes aplicaciones web pueden aislarse entre sí y no pueden ver los archivos de clase que utilizan las demás. Funciona a pesar de que los nombres completos de las clases en diferentes proyectos pueden ser iguales .

inserte la descripción de la imagen aquí

Cuando la aplicación se implementa en caliente, el WebApp ClassLoader original se descartará y se creará un nuevo WebApp ClassLoader para la aplicación.

Blog de referencia

JDBC destruye el modelo de delegación de padres
[0] https://www.jianshu.com/p/09f73af48a98
Puede entenderlo de un vistazo, la explicación detallada de ClassLoader en java súper detallado
[1] https://blog.csdn. net/briblue/article /details/54973413 Comprensión en profundidad
de Java ClassLoader (ClassLoader)
[2]https://blog.csdn.net/javazejian/article/details/73413292Análisis
en profundidad del mecanismo ClassLoader de Java (nivel de fuente)
[3]http://www.hollischuang.com/archives/199 Análisis
detallado y en profundidad del mecanismo de trabajo de Java ClassLoader
[4]https://segmentfault.com/a/1190000008491597Análisis en profundidad del
principio de Java ClassLoader
[5] https://blog.csdn.net/xyang81/ article/details/7292380 Comprensión
profunda de ClassLoader en JVM
[6] https://juejin.im/post/5a27604d51882503dc53938d

Supongo que te gusta

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