Mecanismo de carga de clases y cargador de clases --- máquina virtual Java


Mecanismo de carga de clases y cargador de clases: máquina virtual Java

Cuando se compila un archivo de clase java, se convierte en un archivo de código de bytes con el nombre de clase .class. Pero el archivo .class en este momento no se puede ejecutar inmediatamente, solo cuando se agrega al área de método en la memoria, y después de los datos se verificado, convertido, analizado e inicializado, eventualmente se convierte en un tipo Java que puede ser utilizado directamente por la máquina virtual Java. Este proceso se denomina mecanismo de carga de clases de la máquina virtual Java . Vamos a entenderlo visualmente a través de dos imágenes, La posición del proceso de carga de clases en toda la arquitectura JVM.
Inserte la descripción de la imagen aquí

Como puede ver, hay un subsistema de carga de clases dedicado en la arquitectura de JVM para describir este proceso. Hay dos partes que merecen la comprensión de todos.

  1. Cómo se carga un archivo .class: este es el mecanismo de carga de clases
  2. ¿Qué es responsable de cargar el archivo de clase en: este es el cargador de clases?

Echemos un vistazo más de cerca al subsistema de carga de clases desde los dos aspectos siguientes.

1. Mecanismo de carga de clases:

En el lenguaje Java, la carga, conexión e inicialización de tipos se completan durante el proceso de ejecución del programa. Aunque esto causará un poco de sobrecarga adicional en el lenguaje Java, proporciona una escalabilidad y flexibilidad extremadamente altas para las referencias de Java. Las funciones de lenguaje expandibles dinámicamente se realizan confiando en la carga dinámica y la conexión dinámica durante el tiempo de ejecución.

Un tipo comienza desde que se carga en la memoria de la máquina virtual, hasta que se descarga de la memoria, todo el ciclo de vida pasará por 7 pequeñas etapas:

  1. Cargando
  2. Verificación
  3. Preparación (preparación)
  4. Resolución
  5. Inicialización (inicialización)
  6. Usando (Usando)
  7. Descarga

Las tres etapas de verificación, preparación y análisis se denominan colectivamente Vinculación . Se determina el orden de los cinco procesos de carga, verificación, preparación, inicialización y descarga. A continuación se muestra un diagrama esquemático del ciclo de vida de la clase:

Inserte la descripción de la imagen aquí

1.1 Carga

La carga es solo un proceso de ClassLoading. No lo confunda. En la fase de carga, la máquina virtual Java debe completar las siguientes tres cosas:

  1. Obtenga el archivo de flujo de bytes binarios que define la clase a través del nombre completo de una clase : por ejemplo, el nombre completo de la clase String que todos usan a menudo es java.lang.String
  2. Convierta la estructura de almacenamiento estática representada por el flujo de bytes en la estructura de datos de tiempo de ejecución en el Área de métodos: el archivo de clase se carga principalmente en el Área de métodos
  3. Un objeto java.lang.Class que representa esta clase se genera en la memoria como la entrada de acceso para varios datos de esta clase en el área de método (mecanismo de reflexión): se pueden completar varias operaciones a través del mecanismo de reflexión

1.2 Verificación

La verificación es el primer paso en la fase de conexión. El propósito de esta fase es asegurar que la información en el flujo de bytes de clase cumpla con todos los requisitos de la <especificación de la máquina virtual java>, y que no causará daños a la JVM después de corre.

1.3 Preparación (preparación)

La etapa de preparación es la etapa de asignación formal de memoria y establecimiento de un valor cero (excepto para la última modificación) variables definidas en la clase ** (variables estáticas modificadas con estáticas, también llamadas variables de clase) . Y variables de instancia (es decir, inútiles variables estáticas modificadas) ** se asignarán en el montón de Java hasta que se cree una instancia de un objeto específico.
Inserte la descripción de la imagen aquí

P.ej:

	private static int a=1;//准备阶段分配内存,并且赋初始值0.初始化阶段初始化为1
    private int b=2;//new 一个新对象的时候才在堆中分配内存并赋值
    private final static int c=3;//准备阶段就会分配内存,并且赋初始值3

Es decir, la variable de clase no se asignará según el valor inicial en el código en la fase de preparación, sino que esperará hasta la fase de inicialización. Pero si se modifica con final será diferente. Porque la variable modificada con final se compilará cuando javac genere una propiedad ConstantValue para él, por lo que en la fase de preparación, se asignará de acuerdo con ConstantValue

1.4 Resolución

La fase de análisis es el proceso en el que la JVM reemplaza las referencias simbólicas en el grupo constante con referencias directas.

1.5 Inicialización (Inicialización)

La última acción en el proceso de carga de la clase cuando se inicializa la clase. En los varios procesos anteriores, excepto la participación parcial del cargador definido por el usuario durante la fase de carga, el resto es controlado por la JVM. Hasta que en la fase de inicialización, la JVM realmente comenzará a ejecutar el código del programa Java escrito por el programador en la clase, y dará la iniciativa a la aplicación.

En la fase de preparación, a las variables de clase se les ha asignado valores cero (excepto las constantes), pero en la fase de inicialización, las variables de clase y otros recursos se inicializarán según la codificación del programador. También se puede expresar desde otra perspectiva más intuitiva : inicialización La etapa es el proceso de ejecución del método <clinit> () del constructor de la clase . <clinit> () no lo escribe el programador, pero Javac recopila automáticamente todas las acciones de asignación y los bloques de instrucciones estáticas en la clase (estática ( )) La declaración en <clinit> () es el orden en que aparecen las declaraciones en el archivo fuente.

Por ejemplo, escribimos un fragmento de código y vemos el archivo de código de bytes a través del complemento Jclasslib:

public class ClinitTest
{
    
    
    private int a=0;
    private static int b=1;
    static
    {
    
    
        b=2;
    }
}

Los resultados de la vista Jclasslib son los siguientes:

Inserte la descripción de la imagen aquí

Como puede ver, el valor de b se asigna primero a 1 en orden, y luego el valor de b se asigna a 2. No hay una variable de instancia a.

Aquí hay algunas notas sobre el método <clinit>, pero no lo demostraré.

  • Antes de que se llame al método <clinit> de esta clase, la JVM se asegurará de que se haya ejecutado <clinit> de la clase padre, lo que significa que primero se debe ejecutar <clinit> de la clase Object.
  • El método <clinit> de una clase no es necesario. Si no hay una copia de las variables de clase y los bloques de código estático en una clase, el método <clinit> no se generará para esta clase.
  • Los bloques de código estático no se pueden usar en la interfaz, pero se pueden declarar variables, por lo que el método <clinit> también se puede generar, pero el método <clinit> de la interfaz principal no se ejecutará antes de que se ejecute <clinit> de la interfaz. Y la clase de implementación de la interfaz se está inicializando El método <clinit> de la interfaz no se ejecutará durante el proceso.
  • <clinit> se bloqueará y sincronizará correctamente, porque una clase solo se puede inicializar una vez.

2. Class Loader (ClassLoader)

El código utilizado para realizar la acción de "obtener un flujo de bytes binarios que describa la clase a través del nombre completo de una clase" en la fase de carga se denomina "cargador de clases".

Diagrama esquemático de la estructura del cargador de clases:

Inserte la descripción de la imagen aquí

Mucha gente ve que el cargador de clases es solo un paso en una etapa de la etapa de carga de clases. No puede ser una función más pequeña. ¿Por qué debería tratarse como una parte especial para reservar un libro especial?

De hecho, no es el caso. Desempeña un papel en el programa java mucho más allá de la etapa de carga de clases. Para cualquier clase, el cargador de clases que lo carga y la clase en sí deben determinar su singularidad en la JVM. Cada clase Cargadores todos tener un espacio de nombres de clase independiente. En un lenguaje más popular:

Condición para determinar si dos clases son iguales: (del mismo archivo de clase && cargado por el mismo cargador de clases)

2.1 Tipos de cargadores de clases (JDK1.8)

Desde la perspectiva de la JVM, solo hay dos tipos principales de cargadores de clases:

  • Bootstrap ClassLoader: está implementado en lenguaje C ++ y es parte de la propia JVM
  • Otros cargadores de clases: implementados por el lenguaje Java, independientes de la máquina virtual, heredados de la clase abstracta java.lang.ClassLoader

Pero desde la perspectiva de los desarrolladores de Java, el cargador de clases es más detallado. Java siempre ha mantenido un cargador de clases de tres niveles y una arquitectura de carga de clases delegada por los padres.

Para una clasificación detallada, se puede dividir en 4 tipos, ordenados por prioridad de la siguiente manera, el cargador superior es el cargador de clase padre del siguiente cargador

  1. Bootstrap ClassLoader (Bootstrap ClassLoader)
  2. Cargador de clases de extensión
  3. Application ClassLoader (Application ClassLoader): también conocido como cargador de clases del sistema
  4. Cargador de clases definido por el usuario (Cargador de clases definido por el usuario)

Inserte la descripción de la imagen aquí

2.2 Bootstrap ClassLoader (Bootstrap ClassLoader)

El cargador de clases de inicio está escrito por código C ++ y se usa principalmente para cargar:

  1. Clases principales en <JAVA_HOME> \ lib como rt.jar, clases en tools.jar
  2. O la clase especificada por el parámetro de JVM VM Options: parámetro -Xbootclasspath
ClassLoader classLoader = String.class.getClassLoader();//结果是null,但是不意味着不存在

El resultado es nulo, pero no significa que no exista, porque está escrito en C ++, por lo que no se puede representar en el nivel de Java, y el resultado es nulo. En el cargador de clases JVM, puede usar directamente null para hacer referencia a Bootstrap ClassLoader.

2.3 Extension ClassLoader (Extension ClassLoader)

Este cargador de clases está implementado en código java en la clase sun.misc.Launcher $ ExtClassLoader, y es responsable de cargar:

  1. Directorio <JAVA_HOME> \ lib \ ext
  2. O todas las bibliotecas de clases en la ruta especificada por la variable de sistema java.ext.dirs
        ClassLoader classLoader = SunEC.class.getClassLoader();
        //结果是sun.misc.Launcher$ExtClassLoader@72ea2f77
        System.out.println(classLoader);

2.4 Aplicación ClassLoader (Aplicación ClassLoader)

Este cargador de clases es implementado por sun.misc.Launcher $ AppClassLoader, que es responsable de cargar todas las bibliotecas de clases en la ruta de clases del usuario (ClassPath), es decir, las clases escritas por los usuarios y las bibliotecas de clases de terceros.

        //springboot是第三方的类库
        ClassLoader classLoader = SpringBootCondition.class.getClassLoader();
        //结果是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader);

        //ClassLoaderTest是我自己定义的一个类类
        ClassLoader classLoader1 = new ClassLoaderTest().getClass().getClassLoader();
        //结果是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader1);

        //通过ClassLoader.getSystemClassLoader()可以获取ApplicationClassLoader
        ClassLoader ClassLoader3 = ClassLoader.getSystemClassLoader();
        //结果是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(ClassLoader3);

Y debido a que es el valor de retorno del método estático getSystemClassLoader () en ClassLoader, también se le llama "cargador de clases del sistema".

2.5 Cargador de clases definido por el usuario (Cargador de clases definido por el usuario)

De forma predeterminada, java proporciona 3 tipos de cargadores, pero para algunas situaciones especiales, los programadores pueden personalizar sus propios cargadores de clases y solo necesitan implementar la interfaz ClassLoader.

3. El modelo de delegación matriz y su destrucción

3.1 Modelo de delegación parental

La carga de clases en java usa carga dinámica, es decir, solo cuando se use la clase se cargará el archivo de código de bytes correspondiente.Cuando
un cargador de clases recibe una solicitud de carga de clases, no irá inmediatamente a Cargar, sino que delegará esta solicitud al cargador de clases principal. Hasta que llegue al cargador de Bootstrap superior.
Si el cargador de clases principal puede completar el proceso de carga de la clase, la carga de esta clase se entrega a la clase principal para que la complete. De lo contrario, se delegará a su cargador de subclase.

La siguiente es la implementación del modelo de delegación principal:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
        //1 首先检查类是否被加载
        Class c = findLoadedClass(name);
        if (c == null) {
    
    
            try {
    
    
                if (parent != null) {
    
    
                    //2 没有则调用父类加载器的loadClass()方法;
                    c = parent.loadClass(name, false);
                } else {
    
    
                    //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
    
    
                //4 若父类加载失败,抛出ClassNotFoundException 异常后
                c = findClass(name);
            }
        }
        if (resolve) {
    
    
            //5 再调用自己的findClass() 方法。
            resolveClass(c);
        }
        return c;
    }

3.2 Beneficios del modelo de delegación principal

  1. Evite la carga repetida de clases: las clases en Java y su cargador de clases tienen una relación jerárquica con prioridad. Por ejemplo, la clase java.lang.Object es la clase principal en Java, sin importar qué cargador de clases Para cargar esta clase, eventualmente ser cargado por la clase Bootstrap superior, asegurando así que los Objetos en diferentes entornos sean la misma clase (porque la misma clase tiene dos aspectos).

  2. Proteja la seguridad del programa y evite que la API central sea manipulada arbitrariamente: en programación, a menudo usamos bibliotecas de terceros o clases autodefinidas. Si aparece un nombre completo en el programa

    Para las clases de java.lang.Object, en ausencia de un modelo de delegación principal, aparecerán varias clases de Object, lo que resultará en una confusión del programa.

3.3 Destrucción del modelo de delegación parental

El modelo de delegación principal no es un modelo con restricciones obligatorias, sino una implementación de cargador de clases recomendada por los diseñadores de Java a los desarrolladores. En el mundo de Java, la mayoría de los cargadores de clases siguen este modelo, pero hay excepciones. Hasta la aparición de la modularización de Java (JDK9) , tuvo un total de 3 situaciones "destruidas" a gran escala

  1. El primer "roto" apareció antes del modelo de delegación parental (JDK1.2), porque el modelo de delegación parental se introdujo en JDK1.2, pero el ClassLoader ya existía en JDK1.1. Así que antes de que este modelo se rompiera. Después de JDK1.2 introdujo el modelo de delegación padre, frente al cargador de clases definido por el usuario existente (principalmente reescribiendo el método loadClass), se tuvieron que hacer algunos compromisos. Debido a que el modelo de delegación padre se refleja en loadClass () en el método y JDK1. 1 usuarios han reescrito el método loadClass (), lo que provocó la destrucción del modelo . Por lo tanto, el diseñador definió un nuevo método protegido findClass () en la clase java.lang.ClassLoader. Usuarios de la guía Anulan este método al escribir el mecanismo de carga de clases para que el modelo no será destruido.

    El siguiente código es el método loadClass. Su lógica es el modelo de delegación principal. Puede ver el segundo código if (c == null). Cuando ninguno de los tres cargadores de clases predeterminados de Java completa la carga de la clase Ejecutar definido por el usuario findClass (), que no afectará a los usuarios a cargar clases según sus deseos, y no destruirá el modelo de delegación padre.

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    
    
        synchronized (getClassLoadingLock(name)) {
    
    
            // First, check if the class has already been loaded
            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
                    // 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();
                    c = findClass(name);//程序员重写findClass方法这样就不会破坏模型

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

2. Su segundo daño es causado por sus propios defectos. El modelo de delegación padre resuelve el problema de la consistencia de los tipos básicos cuando cada cargador de clases coopera (los tipos más básicos están determinados por el cargador de clase superior Load), pero si el cargador básico type necesita llamar al código del usuario, ¿qué debemos hacer? En otras palabras: los programadores han implementado una interfaz muy básica con su propio código, lo que significa que esta interfaz básica debe ser hecha por Bootstrap ClassLoader para cargar, pero el cargador de clases sí no reconozco el código del usuario ¿Qué debo hacer?

Para resolver este dilema, el equipo de diseño de Java tuvo que introducir un diseño menos elegante: Thread Context ClassLoader (Thread Context ClassLoader) . Este cargador de clases se puede configurar mediante el método setContextClassLoader () de la clase java.lang.Thread. Si no se establece cuando se crea el subproceso, heredará uno del subproceso principal; si no se ha configurado en el global alcance de la aplicación, entonces este cargador de clases es el cargador de clases de aplicación por defecto, es decir, el cargador de clases avanzado usará el cargador de clases de archivos de subprocesos para cargar el código del usuario, lo que en realidad rompe el modelo de delegación padre.

Inserte la descripción de la imagen aquí

3. La tercera "destrucción" del modelo de delegación parental fue causada por la búsqueda de la dinámica del programa por parte de los usuarios, IBM lanzó OSGi para esto.

El núcleo del despliegue modular en caliente de OSGi radica en la realización de su mecanismo de cargador de clases personalizado. Todos los módulos de programa (paquetes) tienen su propio cargador de clases. Cuando un paquete necesita ser reemplazado, reemplace el paquete junto con el cargador de clases para implementar el reemplazo de código activo. En la ilusión de OSGi, el cargador de clases ya no es una estructura de árbol en el modelo de delegación principal, sino que se ha convertido en una estructura de red más compleja. Cuando se recibe una solicitud de carga de clases, OSGi realizará una búsqueda de clases en el siguiente orden :

  1. Asigne las clases que comienzan con java. * Al cargador de clases principal para cargar.
  2. De lo contrario, asigne las clases en la lista de delegados al cargador de clases principal para que las cargue.
  3. De lo contrario, delegue la clase en la lista de importación al cargador de clases del paquete de la clase de exportación para cargar.
  4. De lo contrario, busque el ClassPath del Bundle actual y cárguelo con su propio cargador de clases.
  5. De lo contrario, averigüe si la clase está en su propio Fragment Bundle y, si lo está, deleguela al cargador de clases del Fragment Bundle para cargarla.
  6. De lo contrario, busque el paquete en la lista de importación dinámica y delegue en el cargador de clases correspondiente al paquete para cargar.
  7. De lo contrario, el cargador de clases falla.

La palabra clave no es fácil, ¿puedes darme un tres en uno para alguien que escribe bien?

Supongo que te gusta

Origin blog.csdn.net/qq_44823898/article/details/111239669
Recomendado
Clasificación