Comprensión profunda de la máquina virtual Java _ capítulo ocho _ motor de ejecución de código de bytes de la máquina virtual

Notas basadas en << Comprensión profunda de la máquina virtual Java >>

Visión general

El motor de ejecución de la máquina física se basa directamente en el nivel del sistema operativo del procesador, la memoria caché y el conjunto de instrucciones.

El motor de ejecución de la máquina virtual se implementa mediante software, que puede personalizar la estructura del conjunto de instrucciones y el motor de ejecución sin estar restringido por condiciones físicas, y puede ejecutar formatos de conjuntos de instrucciones que no son directamente compatibles con el hardware.

Cuando el motor de ejecución ejecuta un código de bytes, hay dos opciones: interpretación, ejecución y compilación. Pero la entrada y la salida son las mismas: el flujo binario del código de bytes de entrada, el proceso de procesamiento es el proceso equivalente al análisis y la ejecución del código de bytes, y la salida es el resultado de la ejecución

Estructura del marco de pila en tiempo de ejecución

La máquina virtual toma un método como la unidad de ejecución más básica, y el marco de la pila corresponde a un método, que es un elemento de la pila de la máquina virtual en el área de datos de la máquina virtual cuando está en ejecución. El proceso de cada método desde el inicio de la llamada hasta el final de la ejecución corresponde al proceso de un marco de pila de la pila a la pila en la pila de la máquina virtual

El marco de pila almacena la tabla de variables locales, la pila de operandos, la conexión dinámica, la dirección de retorno del método y la información adicional.

La cantidad de memoria que se debe asignar para un marco de pila se calcula cuando el código fuente del programa se compila y escribe en el atributo Code de la tabla de métodos. No se verá afectado por los datos variables durante el tiempo de ejecución del programa, sino que solo depende del código fuente del programa y del virtual específico. Disposición de la memoria de pila

Desde la perspectiva de un programa Java, al mismo tiempo, en el mismo hilo, todos los métodos en la pila de llamadas están en el estado de ejecución al mismo tiempo

Para el motor de ejecución, en el subproceso activo, solo se está ejecutando el método en la parte superior de la pila, que se denomina marco de pila actual y método actual

Tabla de variables locales

Es un espacio de almacenamiento para un conjunto de valores de variables, que se utiliza para almacenar los parámetros del método y las variables locales definidas dentro del método.

Cuando el programa Java se compila en un archivo de clase, la capacidad máxima de la tabla de variables locales que debe asignarse al método se determina en el elemento de datos max_locals del atributo Code del método.

La tabla de variables locales es la unidad más pequeña de la ranura variable, una ranura variable puede almacenar un tipo de datos dentro de 32 bits

Se deben hacer al menos dos cosas para citar:

	1.  根据引用直接或间接地查找对象在Java堆中的数据存放的起始地址或索引
	2.  根据引用直接或间接地查找对象所属数据类型在方法区中的存储的类型信息

Para datos de 64 bits, se asignarán dos espacios de ranura variable consecutivos (largo, doble) y los bits divididos se leerán y escribirán dos veces para datos de 32 bits. Debido a que la tabla de variables locales está construida en la pila de subprocesos y pertenece a datos privados de subprocesos, no importa si dos ranuras de variables consecutivas son operaciones atómicas, no causará competencia de datos ni problemas de seguridad de subprocesos.

Cuando se llama al método, la máquina virtual utiliza la tabla de variables locales para completar el proceso de transferencia de valores de parámetros a la lista de variables de parámetros, es decir, la transferencia de parámetros reales a parámetros formales. Si se trata de un método de instancia, la ranura de variable con el índice 0 de la tabla de variables locales almacena la referencia a la instancia de objeto a la que pertenece el método por defecto, es decir, este

Las ranuras variables se pueden reutilizar y las que están fuera del alcance se pueden reasignar

La tabla de variables locales no tiene una etapa de preparación, por lo que si se define una variable local pero no se asigna un valor inicial, no se puede usar. El compilador puede verificar y solicitar este punto durante la compilación.

Pila de operandos

La profundidad máxima también se escribe en max_stacks del atributo Code en tiempo de compilación

La capacidad de la pila ocupada por tipos de datos de 32 bits es 1, y 64 está ocupada por 2, y la profundidad de la pila de operandos no excederá el valor máximo establecido por max_stacks en ningún momento

Al realizar operaciones aritméticas, empujando el operando involucrado en la operación en la parte superior de la pila y luego llamando a la instrucción de operación

Por ejemplo, la instrucción iadd requiere que se hayan almacenado dos tipos de int en los elementos superior y segundo de la pila de operandos en tiempo de ejecución. La ejecución de esta instrucción sacará las dos entradas de la pila y las agregará, y luego las volverá a insertar en la pila.

Enlace dinámico

Para admitir la conexión dinámica durante la invocación del método, cada marco de pila contiene una referencia al método al que pertenece el marco de pila en el grupo de constantes de tiempo de ejecución.

Dirección de devolución del método

Dos formas de salir:

Finalización normal de llamadas: el motor de ejecución encuentra una instrucción de código de bytes devuelta por cualquier método, y luego puede haber un valor de retorno pasado al llamador del método superior

Llamada de excepción completada: se encontró una excepción durante la ejecución del método y no se encontró un controlador de excepciones coincidente en la tabla de excepciones del método

Una vez que el método sale, debe volver a la posición en la que se llamó al método original. Si es una salida normal, se guarda el valor del contador de PC del método principal

Cuando el método sale: restaure la tabla de variables locales y la pila de operandos del método superior, inserte el valor de retorno en la pila de operandos del marco de pila del llamador, ajuste el valor del contador de la PC para que apunte a una instrucción después de la instrucción de llamada al método

información extra

Alguna información que no se describe en la especificación se puede agregar al marco de la pila, como información relacionada con la recopilación de depuración y rendimiento

Por lo general, la conexión dinámica, la dirección de retorno del método y la información adicional se agrupan en una categoría, denominada información del marco de pila.

Llamada de método

Determine la versión del método llamado (es decir, qué método se llama), y el proceso de operación específico dentro del método no está involucrado por el momento

Todas las llamadas a métodos almacenadas en el archivo de clase son solo referencias simbólicas, en lugar de la dirección de entrada del método en el diseño de la memoria en tiempo de ejecución real (es decir, referencia directa)

Por lo tanto, algunas llamadas deben ser durante la carga de la clase o incluso durante el tiempo de ejecución para determinar la referencia directa del método de destino.

Analizando

En la fase de análisis de la clase, algunas de las referencias de símbolos se convertirán en referencias directas, siempre que estos métodos tengan una versión determinable de la llamada antes de que el programa se ejecute realmente, y no cambiará durante el tiempo de ejecución.

Cumplir con "Conocible en tiempo de compilación e inmutable en tiempo de ejecución", existen principalmente dos categorías: métodos estáticos y archivos privados, el primero está directamente relacionado con el tipo y el segundo no se puede acceder externamente.

Llamar instrucción de código de bytes

  • invokestatic se usa para llamar a métodos estáticos
  • invokespecial se usa para llamar al método (), método privado, método de la clase principal
  • invokevirtual se usa para llamar a todos los métodos virtuales
  • invokeinterface se usa para llamar a métodos de interfaz, y un objeto que implementa la interfaz se determinará en tiempo de ejecución
  • Invokedynamic primero resuelve dinámicamente el método al que hace referencia el calificador del sitio de llamada en tiempo de ejecución y luego lo ejecuta

Siempre que el método pueda ser llamado por las instrucciones invocaticestáticas e invocaciones especiales, la versión de llamada única se puede determinar en la fase de análisis.

Hay un total de métodos que cumplen estas condiciones: métodos estáticos, métodos privados, constructores de instancias, métodos padre, además de métodos modificados por final (aunque son modificados por invokevirtual)

Estos cinco tipos de métodos pueden convertir referencias simbólicas en referencias directas del método al cargar la clase, denominados colectivamente métodos no virtuales.

La llamada de resolución debe ser un proceso estático

Envío

Despacho estático

static abstract class Human
{
}
static class Man extends Human
{
}
static class Woman extends Human
{
}

public void sayHello(Human guy)
{
    System.out.println("Human");
}
public void sayHello(Man guy)
{
    System.out.println("Man");
}
public void sayHello(Woman guy)
{
    System.out.println("Women");
}

public static void main(String[] args)
{
    Human man = new Man();
    Human women = new Woman();
    MainTest mainTest = new MainTest();
    mainTest.sayHello(man);
    mainTest.sayHello(women);
    /*
    result:
        Human
        Human
     */
}

Human man = new Man();En, humano se llama tipo estático, hombre se llama tipo real

El tipo estático final se conoce durante la recompilación y el resultado del cambio de tipo real solo se puede determinar en tiempo de ejecución

Cuando la máquina virtual está sobrecargada, el tipo estático del parámetro se usa como base para el juicio en lugar del tipo real. En la fase de compilación, el compilador decide qué versión sobrecargada usar según el tipo estático del parámetro.

Todas las acciones de envío que se basan en tipos estáticos para determinar la versión de ejecución del método se denominan envío estático. La aplicación más típica es la sobrecarga de métodos.

Aunque el compilador puede determinar la versión sobrecargada del método, en muchos casos la versión sobrecargada no es única y solo puede determinar una versión relativamente más adecuada.

Despacho dinámico

Es la realización de la reescritura en polimorfismo.

Distribuya la versión de ejecución del método de acuerdo con el tipo real de variable en tiempo de ejecución.

Field nunca participa en polimorfismo

El lenguaje Java actual es un lenguaje estático de despacho múltiple, lenguaje dinámico de despacho único

Invocar proceso de análisis virtual

  1. Encuentre el tipo real del objeto al que apunta el primer elemento en la parte superior de la pila de operandos, denotado como C
  2. Si un método que coincide con el descriptor y el nombre simple de la constante se encuentra en el tipo C, se realiza la verificación del permiso de acceso y, si pasa, se devuelve la referencia directa de este método y finaliza el proceso de búsqueda. IllegalAccessError se devuelve si falla
  3. De lo contrario, de acuerdo con la relación de herencia, el segundo paso del proceso de búsqueda y verificación se realiza en cada clase principal de C de abajo hacia arriba.
  4. Si no se encuentra un método adecuado, se lanza un AbstractMethodError

Realización de despacho dinámico de máquinas virtuales

Un método de optimización común es crear una tabla de método virtual (vtable) en el área de método y usar el índice de la tabla de método virtual en lugar de metadatos para mejorar el rendimiento de la búsqueda.

La tabla de métodos virtuales almacena la dirección de entrada real de cada método. Si un método no se anula en la subclase, la entrada de direcciones en la tabla de métodos virtuales de la subclase es la misma que la entrada de direcciones del mismo método en la clase principal, y todas apuntan a La entrada de realización de la clase de padres.

Si se reemplaza la subclase, se reemplaza con la dirección de entrada que apunta a la versión de implementación de la subclase.

La tabla de métodos virtuales generalmente se inicializa en la fase de conexión de la carga de clases Después de preparar el valor inicial de la variable de clase, la máquina virtual también inicializa la tabla de métodos virtuales de la clase.

Los métodos predeterminados sin modificación final son métodos virtuales

Compatibilidad con idiomas escritos dinámicamente

Instrucción dinámica invocada, generada para lograr soporte de lenguaje de tipo dinámico

Lenguaje escrito dinámicamente

La característica clave es: el proceso principal de su verificación de tipo se lleva a cabo en tiempo de ejecución en lugar de en tiempo de compilación

Excepción de tiempo de ejecución: mientras el código no se ejecute en esta línea, no se generará ninguna excepción

Excepción durante la conexión: incluso si el código se coloca en una rama de ruta que no se puede ejecutar en absoluto, se lanzará una excepción cuando se cargue la clase

Otra característica central de los lenguajes tipados dinámicamente: las variables no tienen tipo y los valores de las variables tienen tipos

Pros y contras

  1. Los lenguajes tipados estáticamente pueden determinar los tipos de variables durante la compilación, y los compiladores pueden mejorar la verificación de tipos completa y rigurosa, lo que favorece la estabilidad y facilita que los proyectos alcancen escalas más grandes.
  2. El tipo se determina solo durante el tiempo de ejecución de un lenguaje escrito dinámicamente, lo que proporciona a los desarrolladores una gran flexibilidad y claridad, lo que significa que se mejora la eficiencia del desarrollo

java.lang.invoke 包

La diferencia entre MethodHandle y Reflection

  • Reflection es una llamada a método que se simula en el nivel de código Java, y MethodHandle es una llamada a método que simula el nivel de código de bytes
  • Reflection es una imagen completa del lado de Java, que es pesado, y MethodHandle es liviano
  • La reflexión es difícil de optimizar, MethodHandle puede lograr varias optimizaciones (como la inserción de métodos, etc.)
  • Reflection solo sirve al lenguaje Java, y MethodHandle está diseñado para servir a todos los lenguajes de máquinas virtuales Java

Motor de ejecución de interpretación de código de bytes basado en pila

Discuta cómo ejecutar las instrucciones de código de bytes en el método Hay dos tipos de interpretación y ejecución, y compilación y ejecución.

Explicar la ejecución

IMG_20200825_184807

Antes de la ejecución, el análisis léxico y el análisis de sintaxis se realizan en el código fuente del programa, y ​​el código fuente se convierte en un árbol de directorios abstracto.

Conjunto de instrucciones basado en pila y conjunto de instrucciones basado en registro

El flujo de instrucciones de código de bytes generado por el compilador Javac es básicamente una arquitectura de conjunto de instrucciones basada en pilas. La mayor parte del flujo de instrucciones de código de bytes son instrucciones de dirección cero, que dependen de la pila de operandos para funcionar.

Por ejemplo 1 + 1:

iconst_1
iconst_1
iadd
istore_0

Después de que dos instrucciones iconst_1 empujan sucesivamente dos constantes 1 en la pila, la instrucción iadd coloca los dos valores en la parte superior de la pila, los agrega y luego vuelve a colocar el resultado en la parte superior de la pila, y finalmente istore_0 coloca el valor en la parte superior de la pila en la tabla de variables locales En la ranura de la variable 0

Pros y contras

  • La principal ventaja de la pila basada es que es portátil. Con una arquitectura de pila, los programas de usuario no usan registros directamente. Puede ser implementado por una máquina virtual para poner algunos de los datos a los que se accede con más frecuencia (contador de programa, caché superior de pila, etc.) en registros para mejorar el rendimiento . El código es compacto y el compilador es fácil de implementar.
iconst_1
iconst_1
iadd
istore_0

Después de que dos instrucciones iconst_1 empujan sucesivamente dos constantes 1 en la pila, la instrucción iadd coloca los dos valores en la parte superior de la pila, los agrega y luego vuelve a colocar el resultado en la parte superior de la pila, y finalmente istore_0 coloca el valor en la parte superior de la pila en la tabla de variables locales En la ranura de la variable 0

Pros y contras

  • La principal ventaja de la pila basada es que es portátil. Con una arquitectura de pila, los programas de usuario no usan registros directamente. Puede ser implementado por una máquina virtual para poner algunos de los datos a los que se accede con más frecuencia (contador de programa, caché superior de pila, etc.) en registros para mejorar el rendimiento . El código es compacto y el compilador es fácil de implementar.
  • La desventaja es que la velocidad de ejecución es un poco más lenta, la cantidad de instrucciones necesarias para completar la misma función es grande y el acceso frecuente a la pila significa un acceso frecuente a la memoria.

Supongo que te gusta

Origin blog.csdn.net/weixin_42249196/article/details/108253734
Recomendado
Clasificación