El proceso de carga de clases en la máquina virtual Java

El proceso de carga de clases en Java

Por ejemplo, el siguiente código simple

public class HelloWorld {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("我已经被加载啦");
    }
}

¿Cómo es su proceso de carga? El
Inserte la descripción de la imagen aquí
proceso completo se muestra en la siguiente figura:
Inserte la descripción de la imagen aquí
Fase de carga

  • Obtenga el flujo de bytes binarios que define esta clase a través del nombre completo de una clase
  • Convierta la estructura de almacenamiento estática representada por este flujo de bytes en la estructura de datos en tiempo de ejecución del área de métodos
  • Genere un objeto java.lang.Class que represente esta clase en la memoria como la entrada de acceso para varios datos de esta clase

Cómo cargar archivos de clase

  • Cargar directamente desde el sistema local
  • Obtenido a través de Internet, escenario típico: Web Applet
  • Lea del archivo zip y conviértase en la base de los formatos jar y war en el futuro
  • Cálculo y generación en tiempo de ejecución, el más utilizado es: tecnología de proxy dinámico
  • Generado por otros archivos, escenario típico: la aplicación JSP extrae archivos .class de una base de datos propietaria, lo cual es relativamente raro
  • Obtenido de archivos cifrados, medidas de protección típicas contra la descompilación de archivos de clase

Fase de enlace

Verificar

  • El propósito es garantizar que la información contenida en el flujo de bytes del archivo de clase cumpla con los requisitos de la máquina virtual actual, para garantizar la exactitud de la clase cargada y no poner en peligro la seguridad de la máquina virtual en sí.
  • Incluye principalmente cuatro verificaciones, 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 .

Verifique el código de bytes a través de la herramienta Visor binario.
Inserte la descripción de la imagen aquí
Si aparece un archivo de código de bytes ilegal, la verificación fallará.

Preparar

Asigne memoria para la variable de clase y establezca el valor inicial predeterminado de la variable de clase, es decir, valor cero, nulo, etc.

public class HelloWorld {
    
    
    private static int a = 1;  // 准备阶段为0,在下个阶段,也就是初始化的时候才是1
    public static void main(String[] args) {
    
    
        System.out.println(a);
    }
}
  • A la variable a anterior se le asignará un valor inicial en la fase de preparación, pero no es 1, sino 0.
  • La estática modificada con final no se incluye aquí, porque final se asignará durante la compilación y se inicializará explícitamente en la fase de preparación.
  • La variable de instancia no se asignará ni se inicializará aquí, la variable de clase se asignará en el área de método y la variable de instancia se asignará al montón de Java junto con el objeto.

Resolver

  • El proceso de convertir referencias de símbolos en el grupo constante en referencias directas.
  • De hecho, la operación de análisis suele ir acompañada de la JVM después de que se ejecuta la inicialización.
  • La referencia de símbolo es un conjunto de símbolos para describir el objetivo al que se hace referencia. La forma literal de referencia de símbolo está claramente definida en el formato de archivo de clase de la "Especificación de máquina virtual Java". Una referencia directa es un puntero que apunta directamente al objetivo, un desplazamiento relativo o un identificador que está ubicado indirectamente al objetivo.
  • La acción de análisis es principalmente para clases o interfaces, campos, métodos de clase, métodos de interfaz, tipos de métodos, etc. Corresponde a la información de clase CONSTANT, información de referencia de campo CONSTANTE e información de referencia de método constante en el grupo constante.

Fase de inicialización

  • La fase de inicialización es el proceso de ejecución del método constructor de clases. No es necesario definir este método. El compilador javac recopila automáticamente las acciones de asignación de todas las variables de clase en la clase y fusiona las declaraciones en el bloque de código estático (es decir, cuando nuestro código contiene variables estáticas, habrá un método clinit) . Las instrucciones del método constructor se ejecutan en el orden en que aparecen las instrucciones en el archivo fuente.
  • El constructor de clases es diferente del constructor de clases. Si no hay una variable estática, el método de construcción de clases (clinit) no se ejecutará y, después de declarar cualquier clase, se genera un constructor y el valor predeterminado es un constructor de parámetros vacío. El constructor de la clase de ejecución es ejecutar el método init.
public class ClassInitTest {
    
    
    private static int num = 1;
    static {
    
    
        num = 2;
        number = 20;
        System.out.println(num);
        System.out.println(number);  //报错,非法的前向引用
    }

    private static int number = 10;

    public static void main(String[] args) {
    
    
        System.out.println(ClassInitTest.num); // 2
        System.out.println(ClassInitTest.number); // 10
    }
}

Acerca del proceso de asignación de variables cuando la clase principal está involucrada

Si la clase tiene una clase principal, la JVM se asegurará de que la clase principal se haya ejecutado antes de la ejecución de la clase secundaria.

public class ClinitTest1 {
    
    
    static class Father {
    
    
        public static int A = 1;
        static {
    
    
            A = 2;
        }
    }

    static class Son extends Father {
    
    
        public static int b = A;
    }

    public static void main(String[] args) {
    
    
        System.out.println(Son.b);
    }
}

El resultado de salida es 2, lo que significa que cuando se carga ClinitTest1 primero, se encontrará el método principal y luego se ejecutará la inicialización de Son. Sin embargo, Son hereda a Father, por lo que la inicialización de Father debe ejecutarse y se asigna A a 2 al mismo tiempo. Obtenemos el proceso de carga de Father a través de la descompilación. Primero, vemos que el valor original se asigna a 1, luego se copia a 2 y finalmente se devuelve.

iconst_1
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
iconst_2
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
return

La máquina virtual debe asegurarse de que el método de inicialización de una clase esté sincronizado y bloqueado en varios subprocesos.

public class DeadThreadTest {
    
    
    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t 线程t1开始");
            new DeadThread();
        }, "t1").start();

        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t 线程t2开始");
            new DeadThread();
        }, "t2").start();
    }
}
class DeadThread {
    
    
    static {
    
    
        if (true) {
    
    
            System.out.println(Thread.currentThread().getName() + "\t 初始化当前类");
            while(true) {
    
    

            }
        }
    }
}

El resultado del código anterior es:

线程t1开始
线程t2开始
线程t2 初始化当前类

De lo anterior se puede ver que después de la inicialización, solo se puede realizar una inicialización, que es el proceso de bloqueo síncrono.

Supongo que te gusta

Origin blog.csdn.net/qq_33626996/article/details/112829936
Recomendado
Clasificación