Mecanismo de JVM: el tiempo de carga de clases, el proceso de carga de clases, la delegación de padres y la destrucción del modelo de delegación de padres

1. Información general

La máquina virtual JVM carga los datos que describen la clase desde el archivo de clase a la memoria y realiza la verificación, conversión, análisis e inicialización de los datos, y finalmente forma un tipo de Java que la JVM puede usar directamente. Este es el mecanismo de carga de clases de la JVM .

En el lenguaje Java, el proceso de carga de clases, conexión e inicialización se completa durante la ejecución del programa .

2. Momento de la carga de clases

Desde el momento en que se carga la clase en la memoria hasta que se descarga la memoria, el ciclo de vida de la clase incluye 7 procesos de carga , verificación , preparación , análisis , inicialización , uso y descarga .

Mecanismo de JVM: el tiempo de carga de clases, el proceso de carga de clases, la delegación de padres y la destrucción del modelo de delegación de padres

 

  1. ¿En qué circunstancias es necesario iniciar la primera etapa del proceso de carga de clases: carga ? No hay restricciones obligatorias en esta especificación de máquina virtual Java, y la implementación específica de la máquina virtual se puede controlar libremente.
  2. Sin embargo, para la fase de inicialización , la especificación de la máquina virtual estipula estrictamente que solo hay cinco casos en los que la clase debe " inicializarse " inmediatamente (la carga, verificación y preparación se completan antes de esto):

(1) Cuando encuentre las instrucciones de código de 4 bytes de new, getstatic, putstatic o invokestatic , si la clase no se ha inicializado, primero debe activar la inicialización. El escenario de código Java más común para generar estas 4 instrucciones es: cuando se usa la nueva palabra clave para crear una instancia de un objeto, cuando se lee o se configura una variable estática de una clase (modificado por final, el resultado se ha puesto en el grupo constante en tiempo de compilación Excepto para campos constantes estáticos) y al llamar a un método estático de una clase.

(2) Cuando se utiliza el método del paquete java.lang.reflect para realizar una llamada reflexiva a una clase, si la clase no se ha inicializado, primero se debe activar su inicialización.

(3) Cuando se inicializa una clase, si se descubre que el padre no se ha inicializado, debe activar la inicialización de la clase padre.

(4) Cuando se inicia la máquina virtual, el usuario debe especificar una clase principal que se ejecutará (la clase que contiene el método main ()), y la máquina virtual inicializa la clase principal primero.

(5) Cuando se utiliza el soporte de lenguaje dinámico de JDK1.7, si el resultado del análisis final de una instancia de java.lang.invoke.MethodHandle es el identificador de método de REF_getStatic, REF_putStatic, REF_invokeStatic , y la clase correspondiente a este identificador de método no se ha inicializado , Primero debe activar su inicialización.

El comportamiento de los cinco escenarios anteriores se denomina referencia activa a una clase . Además, todas las demás formas de referenciar clases no activarán la inicialización, lo que se denomina referencia pasiva . Los ejemplos comunes de referencias pasivas incluyen: (1) Hacer referencia a las variables estáticas de la clase principal a través de la subclase no provocará que la subclase se inicialice. System.out.println (SubClass.staticValue); (2) Hacer referencia a una clase a través de una definición de matriz no activará la inicialización de esta clase. SuperClass [] arr = new SuperClass [10]; (3)  Las constantes estáticas se almacenan en el grupo de constantes de la clase que llama durante la fase de compilación y, en esencia, no están directamente referenciadas a las constantes definidas. System.out.println (ConstClass.finalStaticValue);

3. El proceso de carga de clases

Todo el proceso de carga de clases de JVM incluye: carga, verificación, preparación, análisis e inicialización.

3.1 Carga

Durante la fase de carga, la JVM debe completar las siguientes 3 cosas:

  1. Obtenga el flujo de bytes binarios que define esta clase a través del nombre completo de una clase (como java.lang.String) .
  2. La estructura de almacenamiento estática representada por este flujo de bytes se transforma en la estructura de datos en tiempo de ejecución del área de métodos .
  3. Una instancia de objeto de java.lang.Class que representa esta clase se genera en la memoria como la entrada de varios accesos a datos en esta clase en el área de métodos .

3.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 contenida en el flujo de bytes del archivo Class cumpla con los requisitos de la máquina virtual actual y no ponga en peligro la seguridad de la máquina virtual en sí . En general, la fase de verificación generalmente completará las siguientes cuatro fases de acciones de verificación: verificación de formato de archivo, verificación de metadatos, verificación de código de bytes y verificación de referencia de símbolo.

3.2.1 Verificación del formato de archivo

La primera etapa consiste en verificar si el flujo de bytes se ajusta a las especificaciones del formato de archivo de clase y puede ser procesado por la versión actual de la máquina virtual.

3.2.2 Verificación de metadatos

La segunda etapa consiste en realizar un análisis semántico sobre la información descrita por el bytecode para asegurar que la información descrita cumple con los requisitos de la especificación del lenguaje Java. Los puntos de verificación que pueden incluirse en esta etapa son los siguientes:

  1. ¿Esta clase tiene una clase principal (excepto java.lang.Object, todas las clases deben tener una clase principal)?
  2. Si la clase principal de esta clase hereda una clase que no puede heredarse (una clase que se modifica por última vez).
  3. Si esta clase no es una clase abstracta, si ha implementado todos los métodos necesarios para ser implementados en su clase padre o interfaz. y muchos más

3.2.3 Verificación del código de bytes

La tercera etapa es la más complicada en todo el proceso de verificación, el objetivo principal es determinar que la semántica del programa es legal y lógica a través del flujo de datos y el análisis del flujo de control.

3.2.4 Verificación de referencia de símbolo

La última etapa de verificación ocurre cuando la máquina virtual convierte las referencias de símbolos en referencias directas, esta acción de conversión ocurrirá en la tercera etapa de la conexión: la fase de análisis . La verificación de referencia de símbolo puede verse como una verificación de coincidencia de información distinta de la clase en sí ( varias referencias de símbolo en el grupo constante  ) .

3.3 Preparación

La etapa de preparación es la etapa de asignar formalmente memoria para variables de clase (variables miembro estáticas) y establecer el valor inicial (valor cero) de las variables de clase, la memoria utilizada por estas variables se asignará en el área de método .

  1. La asignación de memoria solo incluye variables de clase, no variables de instancia, que se asignarán en el montón junto con el objeto cuando se instancia el objeto.
  2. El valor inicial mencionado aquí es el valor cero del tipo de datos en "condiciones habituales". Supongamos que una variable de clase se define como:
public static int value=123;

Entonces, el valor del valor de la variable después de la fase de preparación es 0 en lugar de 123. Debido a que este tiempo aún no ha iniciado ningún método java, y el valor asignado a la instrucción 123 putstatic después de que se compila el programa, se almacena en un método constructor de clase () en, por lo que el valor asignado a la operación de 123 será la fase de inicialización . llevado a cabo.

  1. El "caso especial" es: cuando el atributo de campo del campo de clase es ConstantValue (como una constante estática), se inicializará al valor especificado en la fase de preparación, por lo que después de marcar como final, el valor del valor se inicializa a 123 en lugar de 0 en la fase de preparación.
public static final intvalue =123;

3.4 Análisis

La etapa de análisis es una máquina virtual que a menudo contiene un conjunto de referencias simbólicas reemplazadas por una referencia directa al proceso.

  1. Referencias simbólicas: Las referencias simbólicas utilizan un conjunto de símbolos para describir el objetivo al que se hace referencia. El símbolo puede ser cualquier forma de literal, siempre que se pueda utilizar para localizar el objetivo sin ambigüedad . Referencia de destino y es posible que no se haya cargado en la memoria en formato .
  2. Referencias directas: una referencia directa puede ser un puntero que apunta directamente al objetivo , un desplazamiento relativo o un identificador que puede localizar indirectamente el objetivo . Si hay una referencia directa, el destino al que se hace referencia ya debe existir en la memoria .

3.5 Inicialización

En la fase de inicialización, el código del programa Java (o código de bytes) definido en la clase se ejecuta realmente.

  1. La fase de inicialización es el proceso de ejecutar el método <clinit> () del constructor de clases. El método <clinit> () es generado por el compilador que recopila automáticamente las acciones de asignación de todas las variables de clase en la clase y las declaraciones en el bloque estático (bloque {} estático). El orden en el que el compilador recopila está determinado por el orden en el que aparecen las declaraciones en el archivo fuente. En un bloque de instrucciones estáticas, solo se puede acceder a las variables definidas antes del bloque de declaraciones estáticas. Las variables definidas después de este se pueden utilizar en el bloque de declaraciones estáticas anterior. Asignado, pero no se puede acceder.
    public classTest{
        static { 
            i =0;// 给变量赋值可以正常编译通过System.out.print(i);// 这句编译器会提示“非法向前引用”
        }
        static int i =1;
    }
  1. El método <clinit> () es diferente del constructor de clases (o el método del constructor de instancias <init> ()). No necesita llamar explícitamente al constructor de la clase principal, y se garantiza que la oportunidad virtual estará en la subclase <clinit> () Antes de que se ejecute el método, se ha ejecutado el método <clinit> () de la clase principal. Por lo tanto, la clase del primer método <clinit> () ejecutado en la máquina virtual debe ser java.lang.Object . Dado que el método <clinit> () de la clase principal se ejecuta primero, significa que el bloque de instrucciones estáticas definido en la clase principal tiene prioridad sobre la operación de asignación de variables de la subclase. En el siguiente código, el valor del campo B será 2 en lugar de 1. .
    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }    }    static class Sub extends Parent {
        public static int B = A;
    }    public static void main (String[] args){
        System.out.println(Sub.B); // 输出结果是父类中的静态变量A的值,也就是2。
    }
  1. El método <clinit> () no es necesario para la clase o interfaz. Si no hay un bloque de instrucciones estáticas en una clase y no hay una operación de asignación de variables, entonces el compilador no puede generar el método <clinit> () para esta clase.
  2. Los bloques de instrucciones estáticas no se pueden usar en interfaces, pero todavía hay operaciones de asignación para inicialización de variables, por lo que las interfaces y clases generarán métodos <clinit> (). Pero la diferencia entre una interfaz y una clase es que el método <clinit> () de la interfaz de implementación no necesita ejecutar primero el método <clinit> () de la interfaz principal. La interfaz principal se inicializará solo cuando se utilicen las variables definidas en la interfaz principal. Además, la clase de implementación de la interfaz no ejecutará el método <clinit> () de la interfaz cuando se inicialice.
  3. La máquina virtual garantiza que el método <clinit> () de una clase está correctamente bloqueado y sincronizado en un entorno multiproceso. Si varios subprocesos inicializan una clase al mismo tiempo, solo un subproceso ejecutará el <clinit> () de esta clase. ), Otros subprocesos deben bloquearse y esperar hasta que el subproceso activo termine de ejecutar el método <clinit> (). Si hay una operación que consume mucho tiempo en el método <clinit> () de una clase, puede causar que se bloqueen múltiples procesos [2] En aplicaciones prácticas, este bloqueo a menudo está muy oculto.

4. Modelo de delegación de los padres

4.1 Clases y cargadores de clases

  1. Cargador de clases : el módulo de código que implementa 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 clases se denomina "cargador de clases". El equipo de diseño de la máquina virtual coloca esta acción fuera de la máquina virtual Java para implementar, de modo que la aplicación sienta cómo obtener las clases requeridas.
  2. Para cualquier clase, el cargador de clases que lo carga y la clase misma deben establecer su singularidad en la máquina virtual Java.Cada cargador de clases tiene un espacio de nombre de clase independiente. Comparar si dos clases son "iguales" solo tiene sentido si el mismo cargador de clases carga las dos clases; de lo contrario, incluso si las dos clases se originan en el mismo archivo de clases y las carga la misma máquina virtual Siempre que los cargadores de clases que los cargan sean diferentes, las dos clases deben ser desiguales.

4.2 Tipos de cargadores de clases

Desde la perspectiva de la máquina virtual Java, solo hay dos cargadores de clases diferentes:

  1. Cargador de clases de inicio (Bootstrap ClassLoader), el cargador de clases utiliza el lenguaje C ++, es parte de la propia máquina virtual .
  2. El otro son los cargadores de todas las demás clases , que están implementados por el lenguaje Java, independientemente de la máquina virtual, y todos heredan de la clase abstracta java.lang.ClassLoader .

Desde la perspectiva de los desarrolladores de Java, los cargadores de clases también se pueden dividir en tres tipos de cargadores de clases proporcionados por el sistema y cargadores de clases definidos por el usuario.

  1. Bootstrap ClassLoader: Responsable de cargar y almacenar clases en el directorio <JAVA_HOME> \ lib o en la ruta especificada por el parámetro -Xbootclasspath.
  2. Cargador de clases de extensión (Extension ClassLoader): Este cargador es implementado por sun.misc.Launcher $ ExtClassLoader, que es responsable de cargar el directorio <JAVA_HOME> \ lib \ ext o se especifican las variables del sistema en la ruta java.ext.dirs Para todas las bibliotecas de clases, los desarrolladores pueden usar directamente el cargador de clases extendido.
  3. Cargador de clases de aplicaciones (Application ClassLoader): este cargador de clases es implementado por sun.misc.Launcher $ App-ClassLoader. Dado que este cargador de clases es el valor de retorno del método getSystemClassLoader () en ClassLoader, generalmente se lo denomina cargador de clases del sistema. Es responsable de cargar la biblioteca de clases especificada en la ruta de clases del usuario (ClassPath). Los desarrolladores pueden usar directamente este cargador de clases. Si la aplicación no ha personalizado su propio cargador de clases , esta es generalmente la clase predeterminada en el programa. Cargador .
  4. Cargador de clases personalizado (User ClassLoader): cargador de clases definido por el usuario. Cuando los usuarios escriben su propio cargador de clases, si la solicitud de carga debe delegarse al cargador de clases de arranque, simplemente use null en su lugar. Para crear su propio cargador de clases, solo necesita heredar la clase java.lang.ClassLoader y luego anular su método findClass (String name), es decir, especificar cómo obtener el flujo de código de bytes de la clase.
  • Si desea cumplir con la especificación de delegación parental, vuelva a escribir el método findClass (lógica de carga de clases definida por el usuario ).
  • Si desea destruirlo , vuelva a escribir el método loadClass (la implementación lógica específica de la delegación principal) .

La relación entre estos cargadores de clases se muestra generalmente en la siguiente figura:

Mecanismo de JVM: el tiempo de carga de clases, el proceso de carga de clases, la delegación de padres y la destrucción del modelo de delegación de padres

 

4.3 Modelo de delegación parental

Esta relación jerárquica entre la figura muestra el cargador de clases, denominado modelo de delegación principal del cargador de clases ( modelo de delegación principal ).

  1. 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.
  2. El modelo de delegación padre del cargador de clases se introdujo en JDK 1.2 Cuando no es un modelo de restricción obligatorio, es una implementación del cargador de clases recomendado a los desarrolladores por los diseñadores de Java.

El proceso de trabajo del modelo de delegación padre es:

  1. Si un cargador de clases recibe una solicitud de carga de clases , primero no intentará cargar la clase en sí, sino que delegará la solicitud al cargador de clases principal para completarla.
  2. Esto es cierto para todos los niveles del cargador de clases, por lo que todas las solicitudes de carga de clases deben transmitirse en última instancia al cargador de clases superior .
  3. Solo si el cargador principal informa que no puede completar la solicitud de carga, el cargador secundario intentará cargarlo por sí mismo .

El modelo de delegación padre se puede utilizar para explicar una pregunta: ¿Por qué no puede personalizar la clase java.lang.String?

Respuesta : A través del modelo de delegación principal, sabemos: si el cargador de clases definido por el usuario recibe una solicitud para cargar la clase java.lang.String y la clase java.lang.String no se ha cargado, entonces el cargador de clases personalizado cargará la solicitud Delegado al cargador principal, hasta que se delegue al cargador de clases de inicio (Bootstrcp ClassLoader). El cargador de clases de arranque no reconoce la clase java.lang.String definida por el usuario, solo cargará la clase java.lang.String en el JDK.

4.4 Destruir el modelo de delegación padre

Como se mencionó anteriormente, el modelo de delegación principal no es un modelo de restricción obligatorio , sino una implementación de cargador de clases recomendada por los diseñadores de Java a los desarrolladores. La mayoría de los cargadores de clases en el mundo de Java siguen este modelo, pero hay excepciones: hasta ahora, el modelo de delegación parental ha experimentado principalmente 3 situaciones "rotas" a gran escala.

  1. Para compatibilidad con versiones posteriores, java.lang.ClassLoader después de JDK 1.2 agregó un nuevo método protegido findClass (). Antes de eso, el único propósito de los usuarios para heredar java.lang.ClassLoader era anular el método loadClass (), porque La máquina virtual llamará al método privado loadClassInternal () del cargador cuando esté cargando clases, y la única lógica de este método es llamar a su propio loadClass ().
  2. La segunda destrucción del modelo de delegación padre se debe a los defectos del modelo en sí, que no puede resolver el problema de llamar al código de usuario para las clases básicas.

(1) Es el servicio JNDI (Java Naming and Directory Interface) . El código de JNDI lo carga el cargador de clases de inicio, pero el propósito de JNDI es administrar y buscar recursos de manera centralizada. Debe ser implementado por proveedores independientes y desplegado en la aplicación. El código del proveedor de interfaz JNDI en ClassPath, pero el cargador de clases de inicio puede no reconocer estos códigos. Así que el equipo de diseño de Java presentó: Thread Context ClassLoader. El servicio JNDI utiliza este cargador de clases de contexto de subprocesos para cargar el código SPI requerido, es decir, el cargador de clases padre solicita al cargador de clases hijo que complete la acción de carga de clases, lo que destruye el modelo de delegación padre.

(2) Por ejemplo, tomcat , de acuerdo con la especificación de Java Servlet, se requiere que la prioridad de clase propia de la aplicación web sea mayor que la clase proporcionada por el contenedor web. Para tomcat, para algunas clases no básicas descargadas, el cargador de clases propio de cada aplicación web (WebAppClassLoader) se cargará primero y, cuando no se cargue, se entregará a commonClassLoader para seguir el modelo de delegación principal.

  1. El tercer "destruir" del modelo de delegación de los padres es causado por la búsqueda del usuario de la naturaleza dinámica del programa. Para lograr el intercambio en caliente, la implementación en caliente y la modularidad, significa agregar una función o restar una función sin reiniciar, solo es necesario reemplazar el módulo junto con el cargador de clases para realizar el reemplazo en caliente del código . Por ejemplo, la aparición de OSGi (Open Service Gateway Initiative). En el entorno OSGi, el cargador de clases ya no es una estructura de árbol en el modelo de delegación padre, sino que se desarrolla aún más en una estructura de red.

Supongo que te gusta

Origin blog.csdn.net/qq_45401061/article/details/108761241
Recomendado
Clasificación