Explicación detallada del mecanismo de delegación principal del cargador de clases y la clase

Cargador de clases

Que es un cargador de clases

El equipo de diseño de la máquina virtual coloca la acción de "obtener un flujo de bytes binarios que describa esta clase a través del nombre completo de una clase" en la etapa de carga de la clase fuera de la máquina virtual Java para permitir que la aplicación decida cómo obtener todos La clase requerida. El módulo de código que implementa esta acción se denomina "cargador de clases".

Jerarquía del cargador de clases

imagen-20200827154815302

El modelo de delegación padre requiere que, a excepción del cargador de clases de inicio de nivel superior, todos los demás cargadores de clases deben tener sus propios cargadores de clases padre. Sin embargo, la relación padre-hijo entre los cargadores de clases aquí generalmente no se realiza en una relación de herencia, pero generalmente se usa una relación de composición para reutilizar el código del cargador padre.

Desde la perspectiva de la máquina virtual Java, solo hay dos cargadores de clases diferentes: uno es Bootstrap ClassLoader (Bootstrap ClassLoader), que se implementa en lenguaje C ++ y es parte de la propia máquina virtual; el otro Son todos los demás cargadores de clases, estos cargadores de clases están implementados por el lenguaje Java, independientemente de la máquina virtual, y todos heredan de la clase abstracta java.lang.ClassLoader.

Desde el punto de vista de los desarrolladores de Java, los cargadores de clases se pueden dividir con más detalle. La mayoría de los programas Java utilizan cargadores de clases proporcionados por los siguientes tres sistemas:

Bootstrap ClassLoader (Bootstrap ClassLoader)

Esta clase se almacenará en el directorio <JAVA_HOME> \ lib, o en la ruta especificada por el parámetro -Xbootclasspath, y será reconocida por la máquina virtual (identificada por el nombre del archivo, como rt.jar, tools.jar, La biblioteca de clases cuyo nombre no coincide no se cargará incluso si se coloca en el directorio lib) La biblioteca de clases se carga en la memoria de la máquina virtual.

Cargador de clases de extensión

Este cargador es implementado por sun.misc.Launcher $ ExtClassLoader, que es responsable de cargar todas las bibliotecas de clases en el directorio <JAVA_HOME> \ lib \ ext o en la ruta especificada por la variable de sistema java.ext.dirs. Los desarrolladores pueden directamente Utilice cargador de clases extendido.

Cargador de clases de aplicación

Este cargador de clases está implementado por sun.misc.Launcher $ AppClassLoader. Dado que el cargador de clases de la aplicación es el valor de retorno del método getSystem-ClassLoader () en la clase ClassLoader, también se le llama "cargador de clases del sistema" en algunas ocasiones.

Es responsable de cargar todas las bibliotecas de clases en la ruta de clases del usuario (ClassPath), y los desarrolladores también pueden usar este cargador de clases directamente en el código. Si no ha personalizado su propio cargador de clases en la aplicación, este es generalmente el cargador de clases predeterminado en el programa.

Nuestras aplicaciones se cargan con estos tres tipos de cargador en cooperación entre sí; si es necesario, también puede agregar su propio cargador de clases.

Tres formas de cargar clases

  1. Cargado inicialmente por JVM cuando la aplicación se inicia desde la línea de comandos
  2. Cargar dinámicamente a través del método Class.forName ()
  3. Cargue dinámicamente a través del método ClassLoader.loadClass ()

Ejemplo de código:

public class loaderTest {
    
     
        public static void main(String[] args) throws ClassNotFoundException {
    
     
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
//                Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
//                Class.forName("Test2", false, loader); 
        } 
}

public class Test2 {
    
     
        static {
    
     
                System.out.println("静态初始化块执行了!"); 
        } 
}

Class.forName () 和 ClassLoader.loadClass () 区别

  • Class.forName (): cargue el archivo .class de la clase fuera de jvm, explique la clase y ejecute el bloque estático en la clase;

  • ClassLoader.loadClass (): Solo una cosa es cargar el archivo .class en el jvm, el contenido en el estático no se ejecutará y el bloque estático se ejecutará solo en el newInstance.

  • La función Class.forName (name, initialize, loader) con parámetros también puede controlar si se carga el bloque estático. Y solo cuando se llama al método newInstance (), se llama al constructor para crear el objeto de la clase.

Mecanismo de carga de clases de JVM

Completamente responsable

Cuando un cargador de clases es responsable de cargar una clase, el cargador de clases también cargará otras clases de las que depende la clase y a las que se hace referencia, a menos que se utilice otro cargador de clases para cargarlo.

Delegado padre

Deje que el cargador de clases principal intente cargar la clase primero y solo intente cargar la clase desde su propia ruta de clases cuando el cargador de clases principal no pueda cargar la clase.

Mecanismo de almacenamiento en caché

El mecanismo de almacenamiento en caché garantizará que todas las clases cargadas se almacenen en caché. Cuando se necesita usar una clase en el programa, el cargador de clases primero busca la clase en el área de caché, y solo el área de caché no existe, el sistema leerá la clase correspondiente Los datos binarios se convierten en objetos de clase y se almacenan en el área de búfer. Es por eso que después de modificar la Clase, la JVM debe reiniciarse para que la modificación del programa surta efecto.

Mecanismo de delegación de los padres

El proceso de trabajo del modelo de delegación principal es: si un cargador de clases recibe una solicitud de carga de clases, primero no intentará cargar la clase por sí mismo, sino que delegará la solicitud al cargador de clases principal para completarla. Cada nivel de clase Los cargadores son así, por lo que todas las solicitudes de carga deben transmitirse eventualmente al cargador de clases de inicio de nivel superior, solo cuando el cargador principal informa que no puede completar la solicitud de carga (la clase requerida no se encuentra en su rango de búsqueda) En ese momento, el cargador secundario intentará completar la carga por sí mismo.

Los ejemplos son los siguientes:

  1. Cuando AppClassLoader carga una clase, no intenta primero cargar la clase por sí misma, sino que delega la solicitud de carga de la clase al cargador de clases principal ExtClassLoader para completarla.
  2. Cuando ExtClassLoader carga una clase, no intentará cargar la clase por sí mismo, sino que delega la solicitud de carga de la clase a BootStrapClassLoader para completar.
  3. Si BootStrapClassLoader no se carga (por ejemplo, la clase no se encuentra en $ JAVA_HOME / jre / lib), se usará ExtClassLoader para intentar cargar;
  4. Si ExtClassLoader tampoco se carga, usará AppClassLoader para cargar, si AppClassLoader tampoco se carga, informará una excepción ClassNotFoundException.

Mecanismo de delegación de padres

imagen-20200827135115768

La relación jerárquica entre los cargadores de clases que se muestra en la figura anterior se denomina Modelo de delegación de padres de los cargadores de clases. El modelo de delegación padre requiere que, además del cargador de clases de inicio de nivel superior, todos los demás cargadores de clases deben tener sus propios cargadores de clases padre. Aquí, la relación padre-hijo entre cargadores de clases generalmente no se realiza en la relación de herencia, pero la relación de composición se usa para reutilizar el código del cargador padre .

El modelo de delegación principal del cargador de clases se introdujo durante JDK 1.2 y se usó ampliamente en casi todos los programas de Java posteriormente, pero no es un modelo de restricción obligatorio, sino una clase recomendada por los diseñadores de Java a los desarrolladores. Implementación del cargador.

El proceso de trabajo del modelo de delegación principal es: si un cargador de clases recibe una solicitud de carga de clases, primero no intentará cargar la clase por sí mismo, sino que delegará la solicitud al cargador de clases principal para completarla. Cada nivel de clase Los cargadores son así, por lo que todas las solicitudes de carga deben transmitirse eventualmente al cargador de clases de inicio de nivel superior, solo cuando el cargador principal informa que no puede completar la solicitud de carga (la clase requerida no se encuentra en su rango de búsqueda) En ese momento, el cargador secundario intentará completar la carga por sí mismo.

El uso del modelo de delegación padre para organizar la relación entre cargadores de clases tiene la ventaja obvia de que las clases Java tienen una relación jerárquica de prioridad junto con sus cargadores de clases. Por ejemplo, la clase java.lang.Object, que se almacena en rt.jar, no importa qué cargador de clases quiera cargar esta clase, finalmente se delega al cargador de clases de inicio en la parte superior del modelo para cargar, por lo que la clase Object está en el programa Todos los diversos entornos de cargadores de clases son de la misma clase. Por el contrario, si no se usa el modelo de delegación padre y cada cargador de clases lo carga por sí mismo, si el usuario escribe una clase llamada java.lang.Object y la pone en la ClassPath del programa, habrá más Con una clase de objeto diferente, no se puede garantizar el comportamiento más básico en el sistema de tipos de Java y la aplicación se volverá caótica.

Ventajas de la delegación parental

  • La clase de sistema evita múltiples copias del mismo código de bytes en la memoria
  • Garantizar el funcionamiento seguro y estable de los programas Java.

imagen-20200827135310660

Implementación del código de delegación principal

public Class<?> loadClass(String name)throws ClassNotFoundException {
    
    
            return loadClass(name, false);
    }
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
    
    
            // 首先判断该类型是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
    
    
                //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
    
    
                    if (parent != null) {
    
    
                         //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
    
    
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
    
    
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
    
    
                resolveClass(c);
            }
            return c;
        }
  

La lógica de este código es clara y fácil de entender: primero verifique si el tipo solicitado se ha cargado, si no, llame al método loadClass () del cargador principal, si el cargador principal está vacío, el cargador de clases de inicio se usa como carga principal por defecto Dispositivo. Si el cargador de clases padre no se carga y lanza ClassNotFoundException, entonces llama a su propio método findClass () para intentar cargar.

Cargador de clases personalizado

Normalmente, usamos el cargador de clases del sistema directamente. Sin embargo, a veces, también necesitamos personalizar el cargador de clases. Por ejemplo, la aplicación transmite los códigos de bytes de las clases de Java a través de la red. Para garantizar la seguridad, estos códigos de bytes están encriptados. En este momento, el cargador de clases del sistema no puede cargarlos, por lo que se requiere un cargador de clases personalizado. lograr. Los cargadores de clases personalizados generalmente heredan de la clase ClassLoader. Del análisis anterior del método loadClass, solo necesitamos reescribir el método findClass.

A continuación, usamos un ejemplo para demostrar el proceso del cargador de clases personalizado:

El núcleo del cargador de clases personalizado es obtener archivos de código de bytes. Si es un código de bytes cifrado, el archivo debe descifrarse en esta clase. Dado que esto es solo una demostración, no cifré el archivo de clase, por lo que no hay proceso de descifrado.

public class MyClassLoader extends ClassLoader {
    
    

    private String root;

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

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

    public String getRoot() {
    
    
        return root;
    }

    public void setRoot(String root) {
    
    
        this.root = root;
    }

    public static void main(String[] args)  {
    
    

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\temp");

        Class<?> testClass = null;
        try {
    
    
            testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (InstantiationException e) {
    
    
            e.printStackTrace();
        } catch (IllegalAccessException e) {
    
    
            e.printStackTrace();
        }
    }
}
  

Hay algunas cosas a tener en cuenta aquí :

  1. El nombre de archivo que se pasa aquí debe ser el nombre completo de la clase, es decir, el com.pdai.jvm.classloader.Test2formato, porque el método defineClass se procesa en este formato.
  2. Es mejor no anular el método loadClass, porque es fácil romper el modelo de delegación padre.
  3. Este tipo de clase de prueba en sí puede ser cargado por la clase AppClassLoader, por lo que no podemos poner com / pdai / jvm / classloader / Test2.class en la ruta de clases. De lo contrario, debido a la existencia del mecanismo de delegación principal, AppClassLoader cargará directamente esta clase en lugar de nuestro cargador de clases personalizado.

Supongo que te gusta

Origin blog.csdn.net/kaihuishang666/article/details/108262819
Recomendado
Clasificación