English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Comprender profundamente el proceso completo de carga de clases en Java

Proceso completo de carga de Java

Un archivo Java desde su carga hasta su descarga en este ciclo de vida, debe pasar por4fases:

>Carga->Enlace (verificación+>Preparación+>Análisis->Inicialización (preparación antes del uso)->Uso->Descarga

Carga (excepto la carga personalizada)+El proceso de enlace es responsabilidad exclusiva de JVM, cuándo se debe realizar el trabajo de inicialización de la clase (carga+La enlace se ha completado anteriormente), JVM tiene reglas estrictas (cuatro situaciones):

1. Al encontrar new, getstatic, putstatic, invokestatic, estos4Al ejecutar una instrucción de bytecode, si la clase no ha sido inicializada, se procederá a su inicialización inmediata. En realidad es3Este caso: al instanciar una clase con new, al leer o establecer un campo estático de la clase (excluyendo los campos estáticos marcados con final, ya que ya se han colocado en la cuadrícula de constantes) y al ejecutar métodos estáticos.

2.Usando java.lang.reflect.*.Cuando se realiza una llamada de reflexión a un método de clase, si la clase aún no se ha inicializado, se inicializará de inmediato.

3.Al inicializar una clase, si su padre aún no ha sido inicializado, primero inicializará a su padre.

4.Al iniciar JVM, el usuario debe especificar una clase principal que se va a ejecutar (la clase que contiene static void main(String[] args)), luego JVM inicializará esta clase primero.

De arriba4Este tipo de preprocesamiento se llama una referencia activa a una clase, y todas las otras situaciones, llamadas referencias pasivas, no desencadenarán la inicialización de la clase. A continuación, se dan algunos ejemplos de referencias pasivas:

/** 
 * Escenario de referencia pasiva1 
 * La referencia a un campo estático del padre a través del hijo no causará la inicialización del hijo 
 * @author volador 
 * 
 */ 
class SuperClass{ 
  static{ 
    System.out.println("super class init."); 
  } 
  public static int value=123; 
} 
class SubClass extends SuperClass{ 
  static{ 
    System.out.println("sub class init."); 
  } 
} 
public class test{ 
  public static void main(String[]args){ 
    System.out.println(SubClass.value); 
  } 
} 

El resultado es: super class init.

/** 
 * Escenario de referencia pasiva2 
 * La referencia a una clase a través de una referencia de array no desencadenará la inicialización de esta clase 
 * @author volador 
 * 
 */ 
public class test{ 
  public static void main(String[] args){ 
    SuperClass s_list=new SuperClass[10]; 
  } 
} 

Resultado de salida: sin salida

/** 
 * Escenario de referencia pasiva3 
 * Las constantes se almacenarán en la cuadrícula de constantes de la clase que las llama en la fase de compilación, en esencia no se hace referencia a la clase que define la constante, por lo que naturalmente no se desencadenará la inicialización de la clase que define la constante 
 * @author root 
 * 
 */ 
class ConstClass{ 
  static{ 
    System.out.println("ConstClass init."); 
  } 
  public final static String value="hello"; 
} 
public class test{ 
  public static void main(String[] args){ 
    System.out.println(ConstClass.value); 
  } 
} 

Resultado de salida: hello (consejo: en la compilación, ConstClass.value ya ha sido convertido en la constante hello y se ha colocado en la cuadrícula de constantes de la clase test)

Lo anterior es para la inicialización de la clase, también se debe inicializar la interfaz, la inicialización de la interfaz es ligeramente diferente de la inicialización de la clase:

El código anterior utiliza static{} para imprimir información de inicialización, no se puede hacer con interfaces, pero el compilador aún genera un constructor <clinit>() para las interfaces, que se utiliza para inicializar las variables miembro de la interfaz, esto también se hace en la inicialización de la clase. La verdadera diferencia radica en el tercer punto, antes de que se ejecute la inicialización de la clase, se requiere que se hayan inicializado completamente todas las clases padre, pero la inicialización de la interfaz parece no importar mucho la inicialización de la interfaz padre, es decir, no se requiere que la inicialización de la subinterfaz complete la inicialización de la interfaz padre, solo se inicializará cuando se utilice realmente la interfaz padre (por ejemplo, cuando se cite las constantes de la interfaz).

A continuación, desglosemos el proceso completo de carga de una clase: carga->Verificación->Preparación->Análisis->Inicialización

Primero es la carga:

    Esto es lo que debe completar el motor virtual:3件事:

        1.Obtener el flujo de bytes binario definido por el nombre completo de una clase.

        2.Convertir la estructura de almacenamiento estático representada por este flujo de bytes en la estructura de datos de tiempo de ejecución de la zona de métodos.

        3.Generar un objeto java.lang.Class que representa esta clase en la pila java, como punto de acceso para los datos en la zona de métodos.

Respecto al primer punto, es muy flexible, muchas tecnologías entran aquí, porque no hay un límite para que el flujo binario provenga de donde:

De archivos class->Carga general de archivos

De paquetes zip->Cargar clases de jar

De la red->Applet

..........

En comparación con otros estadios del proceso de carga, el estadio de carga tiene la mayor controlabilidad, porque el cargador de clases puede ser del sistema o personalizado, y los programadores pueden escribir su propio cargador para controlar la obtención de flujos de bytes.

Después de obtener el flujo binario, se almacenará de la manera requerida por el JVM en la zona de métodos, y se instanciará un objeto java.lang.Class en la pila java para asociarlo con los datos en la pila.

Después de que se complete la carga, debe comenzar a verificar estos flujos de bytes (de hecho, muchos pasos se realizan de manera cruzada, como la verificación del formato de archivo):

El objetivo de la verificación: asegurar que la información del flujo de bytes del archivo class sea del agrado del JVM y no le cause incomodidad. Si el archivo class se compila a partir de código Java puro, naturalmente no aparecerán problemas no saludables como desbordamiento de arrays o saltos a bloques de código inexistente, porque si ocurre algo así, el compilador rechazará la compilación. Sin embargo, como se mencionó anteriormente, el flujo de archivo Class no necesariamente proviene de código fuente Java, también puede provenir de la red u otros lugares, e incluso puedes usar16Si el JVM no verifica estos datos, algunos flujos de bytes perjudiciales podrían hacer que el JVM se desintegre completamente.

La verificación principal pasa por varios pasos: verificación de formato de archivo->Verificación de metadatos->Verificación de bytecode->Verificación de referencias simbólicas

Verificación de formato de archivo: verifica si el flujo de bytes cumple con el formato del archivo Class y si su versión puede ser procesada por la versión actual de JVM. Ok, después de verificar, el flujo de bytes se puede guardar en la zona de métodos de la memoria. Después de eso,3Todas las verificaciones se realizan en la zona de métodos.

Verificación de metadatos: análisis semántico de la información descrita en el bytecode, para asegurar que su descripción cumpla con las normas gramaticales del lenguaje Java.

Verificación de bytecode: el más complejo, que verifica el contenido del cuerpo del método, para asegurar que no realice nada extravagante en tiempo de ejecución.

Verificación de referencias simbólicas: para verificar la autenticidad y viabilidad de algunas referencias, como cuando el código se refiere a otras clases, se debe verificar si realmente existen; o cuando el código accede a las propiedades de otras clases, se debe verificar la accesibilidad de esas propiedades. (Este paso establecerá la base para el trabajo de análisis posterior).

La etapa de verificación es importante, pero no necesaria, si algunos códigos se utilizan repetidamente y se han verificado su confiabilidad, la etapa de implementación puede intentar usar-El parámetro Xverify:none se utiliza para desactivar la mayoría de las medidas de verificación de clase, para acortar el tiempo de carga de la clase.

Después de completar los pasos anteriores, se pasará a la etapa de preparación:

Esta etapa asigna memoria a las variables de clase (que se refiere a las variables estáticas) y establece el valor inicial de la clase, esta memoria se asigna en la zona de métodos. Es necesario aclarar que, en esta etapa, solo se establece un valor inicial para las variables estáticas, mientras que las variables de instancia se asignan durante la instanciación del objeto. La asignación de valores iniciales a las variables de clase es ligeramente diferente a la asignación de valores a las variables de clase, por ejemplo:

public static int value=123;

En esta etapa, el valor de value será 0, en lugar de123Porque en este momento aún no se ha comenzado a ejecutar ningún código java,123Aún es invisible, mientras que lo que vemos es123La instrucción putstatic que asigna el valor a value existe después de que el programa se compila en <clinit>(), por lo que, se asigna el valor de123Se ejecuta solo en la etapa de inicialización.

Aquí hay una excepción:

public static final int value=123;

Aquí, en la etapa de preparación, el valor de value se inicializará en123Esto es decir, en la etapa de compilación, javac generará una propiedad ConstantValue para este valor especial y, en la etapa de preparación, jm asignará el valor de ConstantValue a value.

Después de completar el paso anterior, se debe proceder con la análisis. El análisis parece ser una conversión de campos, métodos y otros elementos de la clase, que implica el formato y el contenido del archivo Class, pero no se ha profundizado en su comprensión.

El proceso de inicialización es el último paso del proceso de carga de la clase:}

En el proceso de carga de la clase anterior, además de que el usuario puede participar a través de un cargador de clase personalizado en la fase de carga, las otras acciones están completamente bajo la dirección de la JVM; hasta la fase de inicialización, se comienza a ejecutar realmente el código en java.

Este paso ejecutará algunas operaciones preexistentes, preste atención a la distinción; en la fase de preparación, se ha realizado una asignación de sistema para las variables de clase.

En realidad, este paso es el proceso de ejecutar el método <clinit>(); del programa. Vamos a estudiar el método <clinit>() a continuación:

El método <clinit>() se llama método constructor de la clase, que es la combinación automática del compilador de todas las asignaciones de variables de clase y las sentencias de bloques de código estáticos en la clase, puestas en el mismo orden que en el archivo de origen.

El método <clinit>(); es diferente del método constructor de la clase, no necesita llamar explícitamente al método <clinit>(); del padre, la máquina virtual asegura que el método <clinit>(); del subclase se ejecute antes de que el método de este método del padre se ejecute, es decir, el primer método <clinit>() que se ejecuta en la máquina virtual es el de java.lang.Object.

Vamos a dar un ejemplo para explicar:

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

Primero, Sub.B realiza una referencia a los datos estáticos, Sub debe inicializarse. Al mismo tiempo, su clase padre Parent debe realizarse primero. Después de la inicialización de Parent, A=2,por lo que B=2;el proceso anterior es equivalente a:

static class Parent{ 
  <clinit>(){ 
    public static int A=1; 
    static{ 
      A=2; 
    } 
  } 
} 
static class Sub extends Parent{ 
  <clinit>(){ //El jvm primero permitirá que se ejecute el método de la clase padre y luego aquí 
  public static int B=A; 
  } 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

El método <clinit>(); no es necesario para las clases ni los interfaces; si neither la clase ni el interfaz asignan valores a las variables de clase ni tienen bloques de código estáticos, el método <clinit>() no será generado por el compilador.

Debido a que no se puede existir un bloque de código estático de tipo static{} en el interfaz, pero aún puede existir la operación de asignación de variables durante la inicialización de variables, por lo que también se generará el constructor <clinit>() en el interfaz. Pero a diferencia de las clases, no es necesario ejecutar el método <clinit>(); del padre del interfaz antes de ejecutar el método <clinit>(); del subinterfaz; cuando se utilice una variable definida en el interfaz padre, se inicializará el interfaz padre.

Además, la clase que implementa una interfaz también no ejecutará el método <clinit>() de la interfaz en el momento de la inicialización.]}

Además, el jvm asegura que el método <clinit>(); de una clase en un entorno de múltiples hilos pueda ser sincronizado correctamente. <Porque la inicialización solo se ejecutará una vez>.

Vamos a explicar con un ejemplo:

public class DeadLoopClass { 
  static{ 
    if(true){ 
    System.out.println("["+Thread.currentThread()+"]; Inicializado, ahora viene un bucle infinito"); 
    while(treu){}   
    } 
  } 
  /** 
   * @param args 
   */ 
  public static void main(String[] args) { 
    // TODO Auto-espalda metódica generada 
    System.out.println("toplaile"); 
    Runnable run=new Runnable(){ 
      @Override 
      public void run() { 
        // TODO Auto-espalda metódica generada 
        System.out.println("["+Thread.currentThread()+"]; Vamos a instanciar esa clase"); 
        DeadLoopClass d=new DeadLoopClass(); 
        System.out.println("["+Thread.currentThread()+"); Finalizó la inicialización de esa clase"); 
      }}; 
      new Thread(run).start(); 
      new Thread(run).start(); 
  } 
} 

Aquí, al ejecutarse, verás el fenómeno de bloqueo.

Gracias por leer, espero que pueda ayudar a todos, gracias por el apoyo a este sitio!

Te gustará