JVM2: comprensión profunda del mecanismo de trabajo de ClassLoader

1. ¿Qué es ClassLoader?

ClassLoader, como su nombre lo indica, es un cargador de clases. Funciones de ClassLoader :

  • Responsable de cargar la clase en la JVM
  • Revisar quién carga cada clase (mecanismo de carga jerárquico de padres primero)
  • Vuelva a analizar el código de bytes de la clase en el formato de objeto requerido por la JVM

Carga lenta

La ejecución de JVM no carga todas las clases necesarias a la vez, se carga bajo demanda, es decir, carga diferida. Cuando el programa se está ejecutando, encontrará gradualmente muchas clases nuevas que no reconoce, y luego se llamará a ClassLoader para cargar estas clases. Una vez completada la carga, el objeto Class se almacenará en ClassLoader y no es necesario volver a cargarlo la próxima vez.

Por ejemplo, cuando llama a un método estático de una determinada clase, la clase debe cargarse primero, pero el campo de instancia de esta clase no se tocará. Luego, la clase Clase del campo de instancia no necesita cargarse temporalmente, sino puede ser Categorías de carga relacionadas con campos estáticos, porque los métodos estáticos accederán a los campos estáticos. Es posible que la categoría del campo de instancia no se cargue hasta que cree una instancia del objeto.

Tiempo y proceso de carga de clases

Una clase comienza desde que se carga en la memoria de la máquina virtual hasta que se descarga de la memoria. Su ciclo de vida completo incluye las siete etapas de carga, verificación, preparación, análisis, inicialización, uso y descarga. Entre ellos, las tres partes de verificación, preparación y análisis se denominan colectivamente vinculación.
Escriba la descripción de la imagen aquí

 

Dos, los tres ClassLoaders proporcionados por Java por defecto

Habrá múltiples ClassLoaders en la instancia en ejecución de JVM, y diferentes ClassLoaders cargarán archivos de código de bytes desde diferentes lugares. Se puede cargar desde diferentes directorios de archivos, diferentes archivos jar o diferentes direcciones de servicio en la red. Tres ClassLoaders importantes están integrados en la JVM, a saber, BootstrapClassLoader, ExtensionClassLoader y AppClassLoader.

1.BootstrapClassLoader

Responsable de cargar las clases principales del tiempo de ejecución de JVM. Estas clases se encuentran en el archivo JAVA_HOME / lib / rt.jar. Nuestras bibliotecas integradas de uso común java.xxx. * Están todas en él, como java.util. * , java.io. *, java.nio. *, java.lang. *, etc. Este ClassLoader es especial, está implementado por código C, lo llamamos "cargador raíz".

2.ExtensionClassLoader

Responsable de cargar las clases de extensión de JVM, como la serie swing, el motor js incorporado, el analizador xml, etc. Estos nombres de biblioteca generalmente comienzan con javax, y sus paquetes jar se encuentran en JAVA_HOME / lib / ext / *. Jar, y allí Hay muchos envases de jarras.

3.AppClassLoader

Es el cargador directamente frente a nuestros usuarios, cargará los paquetes y directorios jar en la ruta definida en la variable de entorno Classpath. El código que escribimos y los paquetes jar de terceros que usamos generalmente son cargados por él.

Para esos paquetes jar y archivos de clase proporcionados por servidores de archivos estáticos en la red, jdk tiene un URLClassLoader incorporado. Los usuarios solo necesitan pasar una ruta de red estandarizada al constructor y luego usar URLClassLoader para cargar bibliotecas de clases remotas. URLClassLoader no solo puede cargar bibliotecas de clases remotas, sino también cargar bibliotecas de clases en la ruta local, dependiendo de las diferentes formas de dirección en el constructor. ExtensionClassLoader y AppClassLoader son subclases de URLClassLoader y cargan bibliotecas de clases desde el sistema de archivos local.

AppClassLoader puede obtenerse mediante el método estático getSystemClassLoader () proporcionado por la clase ClassLoader, es lo que llamamos "cargador de clases del sistema", y el código de clase que suelen escribir nuestros usuarios suele ser cargado por él. Cuando se ejecuta nuestro método principal, el cargador de la primera clase de usuario es AppClassLoader.

Tres. Transitividad de ClassLoader

Cuando el programa se está ejecutando, encuentra una clase desconocida. ¿Qué ClassLoader elegirá para cargarlo? La estrategia de la máquina virtual es utilizar el ClassLoader del objeto Class de la persona que llama para cargar la clase actualmente desconocida. ¿Qué es el objeto de clase del llamador? Es decir, cuando se encuentra con esta clase desconocida, la máquina virtual debe estar ejecutando una llamada de método (método estático o método de instancia). En qué clase se cuelga este método, entonces esta clase es el objeto de clase que llama. Anteriormente mencionamos que cada objeto Class tiene una propiedad classLoader que registra quién carga la clase actual.

Debido a la transitividad de ClassLoader, todas las clases de carga retrasada serán totalmente responsables del ClassLoader que inicialmente llama al método principal, que es AppClassLoader.

Cuatro. Delegación de los padres

Anteriormente mencionamos que AppClassLoader solo es responsable de cargar bibliotecas de clases en Classpath. Si encuentra una biblioteca de clases del sistema que no está cargada, AppClassLoader debe transferir la carga de la biblioteca de clases del sistema a BootstrapClassLoader y ExtensionClassLoader. Esto es lo que solemos decir " Cita por ambos padres ".

1. La arquitectura de ClassLoader:

 

Cuando AppClassLoader carga un nombre de clase desconocido, no busca en Classpath inmediatamente. Primero entregará el nombre de clase a ExtensionClassLoader para que lo cargue. Si se puede cargar ExtensionClassLoader, entonces AppClassLoader no tiene que molestarse. De lo contrario, buscará Classpath.

Cuando ExtensionClassLoader carga un nombre de clase desconocido, no busca inmediatamente la ruta ext. Primero entregará el nombre de la clase a BootstrapClassLoader para que lo cargue. Si se puede cargar BootstrapClassLoader, ExtensionClassLoader no tiene por qué ser molesto. De lo contrario, buscará el paquete jar en la ruta ext.

Se forma una relación en cascada entre padres e hijos entre estos tres ClassLoaders. Cada ClassLoader es muy vago. Trate de dejarle el trabajo al padre. El padre no puede hacerlo por sí mismo. Cada objeto ClassLoader tendrá una propiedad principal que apunta a su cargador principal. Cuando podamos cargar esta clase, se cargará correctamente; de ​​lo contrario, se lanzará una excepción, ClassNotFound no se puede encontrar

También habrá cooperación entre diferentes ClassLoaders, que se logra mediante el atributo principal y el mecanismo de delegación principal. El padre tiene una prioridad de carga más alta. Además, el padre también expresa una relación compartida.Cuando varios ClassLoaders secundarios comparten el mismo padre, entonces las clases contenidas en el padre pueden considerarse compartidas por todos los ClassLoaders secundarios. Esta es la razón por la que BootstrapClassLoader es considerado como un cargador ancestro por todos los cargadores de clases, y la biblioteca de clases principal de JVM debería compartirse naturalmente.

2. ¿Por qué necesitamos un mecanismo como la delegación de los padres? ¿No es mi cargador más simple y sencillo?

Principalmente por seguridad, si escribe un cargador de clases personalizado para cargar java.lang.string en la memoria. Empaquetado para clientes. Luego, almacene la contraseña como un objeto de tipo String, luego puedo enviarme la contraseña en secreto, no es seguro.

La delegación de los padres no tendrá tal problema. Cuando el cargador de clases personalizado carga la cadena, se pone alerta. Primero va a lo anterior para verificar si está cargada, y lo anterior cargado regresa directamente a usted sin recargar.

En este momento, Kong Jing vino a levantar el kong, el código fue escrito por mí, no puedo simplemente grabarlo cuando él lo ingrese, o copiar la base de datos directamente, jaja, te daré una explosión. Ese es el problema. de escribir el código, y te lo daré Un par de brazaletes plateados más almuerzo gratis, nada que ver con la seguridad de JVM

¿Cómo romper la delegación de los padres, por qué romperla?

3.Clase.porNombre

Cuando usamos el controlador jdbc, a menudo usamos el método Class.forName para cargar dinámicamente la clase del controlador.

Class.forName("com.mysql.cj.jdbc.Driver");


El principio es que hay un bloque de código estático en la clase Driver controlado por mysql, que se ejecutará cuando se cargue la clase Driver. Este bloque de código estático registrará la instancia del controlador mysql en el administrador de controladores jdbc global.

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


El método forName también usa el ClassLoader del objeto Class del llamador para cargar la clase de destino. Pero forName también proporciona una versión de múltiples parámetros, puede especificar qué ClassLoader cargar

Class<?> forName(String name, boolean initialize, ClassLoader cl)


Esta forma de método forName puede superar las limitaciones del cargador integrado y, al usar un cargador de clases personalizado, nos permite cargar bibliotecas de clases libremente desde cualquier otra fuente. De acuerdo con la transitividad de ClassLoader, otras bibliotecas de clases a las que hace referencia la biblioteca de clases de destino también se cargarán utilizando un cargador personalizado.

Five. Cargador personalizado

Hay tres métodos importantes: loadClass (), findClass () y defineClass () en ClassLoader.

El método loadClass () es el punto de entrada para cargar la clase de destino. Primero descubrirá si la clase de destino ya está cargada en el ClassLoader actual y sus padres. Si no se encuentra, permitirá que los padres intenten cargar. Si los padres no pueden cargar, llamará a findClass () Deje que el cargador personalizado cargue la clase de destino. El método findClass () de ClassLoader debe estar cubierto por subclases. Diferentes cargadores usarán una lógica diferente para obtener el código de bytes de la clase de destino. Después de obtener este bytecode, llame al método defineClass () para convertir el bytecode en un objeto Class. 

import com.mashibing.jvm.Hello;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class T007_MSBClassLoaderWithEncription extends ClassLoader {

    public static int seed = 0B10110110;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("c:/test/", name.replace('.', '/').concat(".msbclass"));

        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b ^ seed);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

    public static void main(String[] args) throws Exception {

        encFile("com.mashibing.jvm.hello");

        ClassLoader l = new T007_MSBClassLoaderWithEncription();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
    }

    private static void encFile(String name) throws Exception {
        File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".msbclass")));
        int b = 0;

        while((b = fis.read()) != -1) {
            fos.write(b ^ seed);
        }

        fis.close();
        fos.close();
    }


El cargador de clases personalizado no es fácil de romper las reglas de delegación parental y no anula fácilmente el método loadClass. De lo contrario, es posible que el cargador personalizado no pueda cargar la biblioteca de clases principal incorporada. Cuando utilice un cargador personalizado, debe dejar en claro quién es su cargador principal y pasar el cargador principal a través del constructor de la subclase. Si el cargador de clases principal es nulo, significa que el cargador principal es el "cargador raíz".

// ClassLoader 构造器
protected ClassLoader(String name, ClassLoader parent);


La regla de delegación parental puede convertirse en la delegación del tercer padre, la delegación del cuarto padre, según el cargador principal que utilice, siempre se delegará de forma recursiva al cargador raíz.

Class.forName vs ClassLoader.loadClass

Ambos métodos se pueden usar para cargar la clase de destino. Hay una pequeña diferencia entre ellos, es decir, el método Class.forName () puede obtener la clase del tipo nativo, mientras que ClassLoader.loadClass () reportará un error.

Class<?> x = Class.forName("[I");
System.out.println(x);

x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);

---------------------
class [I

Exception in thread "main" java.lang.ClassNotFoundException: [I
...

Dependencia del diamante

Existe un concepto bien conocido en la gestión de proyectos llamado "Dependencia Diamante", que significa que las dependencias de software hacen que dos versiones del mismo paquete de software coexistan y no entren en conflicto.

 

 


El maven que usamos normalmente resuelve la dependencia de diamante de esta manera. Escogerá una de las múltiples versiones en conflicto para usar. Si la compatibilidad entre las diferentes versiones es mala, el programa no podrá compilarse y ejecutarse normalmente. Esta forma de Maven se denomina gestión de dependencias "plana".

 

El uso de ClassLoader puede resolver el problema de la dependencia del diamante. Las diferentes versiones de los paquetes de software usan diferentes ClassLoaders para cargar, y las clases con el mismo nombre ubicadas en diferentes ClassLoaders son en realidad clases diferentes . Usemos URLClassLoader para probar un ejemplo simple, su cargador principal predeterminado es AppClassLoader

$ cat ~/source/jcl/v1/Dep.java
public class Dep {
    public void print() {
        System.out.println("v1");
    }
}

$ cat ~/source/jcl/v2/Dep.java
public class Dep {
  public void print() {
    System.out.println("v1");
  }
}

$ cat ~/source/jcl/Test.java
public class Test {
    public static void main(String[] args) throws Exception {
        String v1dir = "file:///Users/qianwp/source/jcl/v1/";
        String v2dir = "file:///Users/qianwp/source/jcl/v2/";
        URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
        URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});

        Class<?> depv1Class = v1.loadClass("Dep");
        Object depv1 = depv1Class.getConstructor().newInstance();
        depv1Class.getMethod("print").invoke(depv1);

        Class<?> depv2Class = v2.loadClass("Dep");
        Object depv2 = depv2Class.getConstructor().newInstance();
        depv2Class.getMethod("print").invoke(depv2);

        System.out.println(depv1Class.equals(depv2Class));
   }
}

Antes de ejecutar, necesitamos compilar las bibliotecas dependientes.

$ cd ~/source/jcl/v1
$ javac Dep.java
$ cd ~/source/jcl/v2
$ javac Dep.java
$ cd ~/source/jcl
$ javac Test.java
$ java Test
v1
v2
false


En este ejemplo, si los dos URLClassLoaders apuntan a la misma ruta, la siguiente expresión sigue siendo falsa, porque incluso el mismo código de bytes cargado con diferentes clases de ClassLoader no se puede considerar la misma clase

depv1Class.equals(depv2Class)


También podemos hacer que dos versiones diferentes de la clase Dep implementen la misma interfaz, lo que puede evitar el uso de la reflexión para llamar a los métodos en la clase Dep.

Class<?> depv1Class = v1.loadClass("Dep");
IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
depv1.print()


Aunque ClassLoader puede resolver el problema del conflicto de dependencia, también limita la interfaz de operación de diferentes paquetes de software para ser llamados dinámicamente por reflexión o interfaz. Maven no tiene esta restricción. Se basa en la estrategia de carga diferida predeterminada de la máquina virtual. Si el ClassLoader personalizado no se muestra durante la operación, el AppClassLoader se usa de principio a fin y se deben cargar diferentes versiones de la misma clase de nombre con diferentes ClassLoader, por lo que Maven no puede resolver perfectamente la dependencia del diamante.

Si desea saber si existe una herramienta de administración de paquetes de código abierto que pueda resolver la dependencia del diamante, le recomiendo que aprenda sobre sofa-ark, que es un marco de aislamiento de clase liviano de código abierto de Ant Financial.

 

Thread.contextClassLoader

Si lee un poco el código fuente de Thread, encontrará un campo muy especial en su campo de instancia

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}


contextClassLoader "cargador de clases de contexto de subprocesos", ¿qué es exactamente esto?

En primer lugar, contextClassLoader es el tipo de cargador de clases que debe usarse explícitamente. Si no lo usa explícitamente, nunca lo usará en ningún lado. Puede utilizar la siguiente forma para mostrar el uso de la misma

Thread.currentThread().getContextClassLoader().loadClass(name);


Esto significa que si usa el método forName (nombre de cadena) para cargar la clase de destino, no usará automáticamente el contextClassLoader. Las clases que se cargan de forma diferida debido a dependencias de código no se cargarán automáticamente usando contextClassLoader.

En segundo lugar, el contextClassLoader del hilo se hereda del hilo padre. El llamado hilo padre es el hilo que creó el hilo actual. El contextClassLoader del hilo principal cuando se inicia el programa es AppClassLoader. Esto significa que si no hay una configuración manual, el contextClassLoader de todos los subprocesos es AppClassLoader.

Entonces, ¿para qué se usa exactamente este contextClassLoader? Tenemos que utilizar el principio de división y cooperación de los cargadores de clases mencionado anteriormente para explicar su propósito.

Puede compartir clases entre subprocesos, siempre que compartan el mismo contextClassLoader. El contextClassLoader se pasará automáticamente entre los subprocesos principal y secundario, por lo que el intercambio será automatizado.

Si diferentes subprocesos usan diferentes contextClassLoader, entonces las clases utilizadas por diferentes subprocesos se pueden aislar.

Si dividimos el negocio, diferentes empresas usan diferentes grupos de subprocesos, el mismo contextClassLoader se comparte dentro de los grupos de subprocesos y se utilizan diferentes contextClassLoaders entre grupos de subprocesos, lo que puede desempeñar un buen papel en la protección de aislamiento y evitar conflictos de versiones de clases.

Si no personalizamos el contextClassLoader, todos los subprocesos usarán AppClassLoader de forma predeterminada y todas las clases se compartirán.

 

Thread contextClassLoader se usa en raras ocasiones. Si la lógica anterior es oscura, no se preocupe demasiado.

Después de que JDK9 agregó la función del módulo, el diseño estructural del cargador de clases se modificó hasta cierto punto, pero el principio del cargador de clases sigue siendo similar. Como contenedor de clases, desempeña un papel de aislamiento de clases y también debe depender en el mecanismo de delegación padre Establecer una relación de cooperación entre diferentes cargadores de clases.

referencia:

1. El antiguo y difícil ClassLoader de Java, es hora de comprenderlo por completo 

2. Explicación detallada de CLASSLOADER

3. Comprenda profundamente el mecanismo de trabajo de ClassLoader (jdk1.8)

Supongo que te gusta

Origin blog.csdn.net/zhaofuqiangmycomm/article/details/113825099
Recomendado
Clasificación