Comprensión profunda de la máquina virtual JVM 5: motor de ejecución de código de bytes de la máquina virtual

1. Información general

El motor de ejecución es uno de los componentes centrales de la máquina virtual Java. El motor de ejecución de la máquina virtual se implementa por sí mismo, por lo que puede personalizar la estructura del conjunto de instrucciones y el motor de ejecución, y puede ejecutar aquellos formatos de conjuntos de instrucciones que no son directamente compatibles con el hardware.

Los motores de ejecución de todas las máquinas virtuales Java son los mismos: la entrada es un archivo de código de bytes, el proceso de procesamiento es el proceso equivalente al análisis de código de bytes y la salida es el resultado de la ejecución . Esta sección explicará principalmente la llamada al método y la ejecución del código de bytes de la máquina virtual desde la perspectiva del modelo conceptual .

2 Estructura del marco de pila en tiempo de ejecución

Stack frame  es una estructura de datos que se utiliza para admitir llamadas a métodos de máquinas virtuales y ejecución de métodos. Es un elemento de pila de la pila de máquinas virtuales (Virtual Machine Stack) en el área de datos del tiempo de ejecución de la máquina virtual .

El marco de pila almacena la tabla de variables locales del método, la pila de operandos, la conexión dinámica y la dirección de retorno del método y otra información. El proceso de cada método desde el inicio de la llamada hasta la finalización 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.

La estructura conceptual del marco de la pila se muestra en la siguiente figura:

Estructura conceptual del marco de la pila

2.1 Tabla de variables locales

La tabla de variables locales es un grupo de espacio de almacenamiento de valores variables que se utiliza para almacenar los parámetros del método y las variables locales definidas en el método.
La capacidad de la tabla de variables locales utiliza la ranura variable como la unidad más pequeña.
 Una ranura puede almacenar un tipo de datos dentro de 32 bits (booleano, byte, char, short, int, float, reference y returnAddress). El tipo de referencia representa una referencia a una instancia de objeto. ReturnAddress es poco común y puede ignorarse.

Para los tipos de datos de 64 bits (los únicos tipos de datos de 64 bits claramente definidos en el lenguaje Java son largos y dobles), la máquina virtual le asigna dos espacios de ranura consecutivos de una manera altamente alineada.

La máquina virtual usa la tabla de variables locales por posicionamiento de índice , y el valor del índice varía de 0 al número máximo de ranuras en la tabla de variables locales. La variable a la que se accede es un tipo de datos de 32 bits. El índice n representa el uso de la n-ésima ranura. Si es un tipo de datos de 64 bits, significa que ambas ranuras n y n + 1 se utilizarán al mismo tiempo.

Para ahorrar el espacio del marco de la pila, la variable local Slot se puede reutilizar y el alcance de la variable definida en el cuerpo del método no cubre necesariamente todo el cuerpo del método. Si el valor actual del contador de PC de código de bytes excede el alcance de una determinada variable, entonces la ranura de esta variable puede ser utilizada por otras variables. Tal diseño traerá algunos efectos secundarios adicionales, tales como: en algunos casos, la reutilización de ranuras afectará directamente el comportamiento de recolección del sistema.

2.2 Pila de operandos

La pila de operandos (Operand Stack)  también se denomina pila de operaciones, es una pila de último en entrar , primero en salir . Cuando comienza la ejecución de un método, la pila de operandos de este método está vacía Durante la ejecución del método, habrá varias instrucciones de código de bytes para escribir y extraer contenido de la pila de operandos, es decir, operaciones de extracción / extracción  .

Pila de operandos

En el modelo conceptual, dos marcos de pila en un hilo activo son independientes entre sí. Sin embargo, la mayoría de las implementaciones de máquinas virtuales realizarán algunas optimizaciones: deje que parte de la pila de operandos del siguiente marco de pila se superponga con parte de la tabla de variables locales del marco de pila anterior. La ventaja de esto es que parte de los datos se pueden compartir cuando se llama al método y no es necesario realizar una transferencia de copia de parámetros adicional.

2.3 Conexión dinámica

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 Esta referencia se mantiene para admitir la conexión dinámica durante la invocación del método ;

La instrucción de llamada al método en el código de bytes toma la referencia de símbolo al método en el grupo constante como parámetro. Algunas referencias de símbolo se convertirán en referencias directas durante la etapa de carga de la clase o la primera vez que se utilicen. Esta conversión se denomina  resolución estática . , La otra parte se convierte en referencia directa durante cada ejecución, esta parte se llama conexión dinámica .

2.4 Método de dirección de devolución

Cuando se ejecuta un método, hay dos formas de salir del método:

  • La primera es que el motor de ejecución encuentra una instrucción de código de bytes devuelta por cualquier método.Este método de salida se denomina Compleción de invocación de método normal .

  • La otra es que se encuentra una excepción durante la ejecución del método, y la excepción no se procesa en el cuerpo del método (es decir, no hay un manejador de excepciones coincidente en la tabla de manejo de excepciones de este método), lo que hará que el método Este método de salida se denomina Finalización de invocación de método abrupto (Finalización de invocación de método abrupto) .
    Nota: Este método de salida no generará ningún valor de retorno para el llamador superior.

Independientemente del método de salida que se utilice, después de que el método salga, debe volver al lugar donde se llamó al método antes de que el programa pueda continuar ejecutándose . Cuando el método regresa, es posible que deba guardar cierta información en el marco de pila para ayudar a restaurar la ejecución de su método superior. En términos generales, cuando el método sale normalmente, el valor del contador de PC de la persona que llama se puede usar como la dirección de retorno, y es probable que este valor del contador se guarde en el marco de la pila. Cuando el método sale de forma anormal, la dirección de retorno está determinada por la tabla del manejador de excepciones, y esta parte de la información generalmente no se guarda en el marco de la pila.

El proceso de salida del método es en realidad equivalente a hacer estallar el marco de pila actual, por lo que las operaciones que se pueden realizar al salir son: restaurar la tabla de variables locales y la pila de operandos del método superior, y empujar el valor de retorno (si lo hay) en el pila de llamadas En la pila de operandos de la trama, ajuste el valor del contador de PC para que apunte a una instrucción después de la instrucción de llamada al método, etc.

2.5 Información adicional

La especificación de la máquina virtual permite que la implementación de la máquina virtual agregue información adicional personalizada al marco de la pila, como información relacionada con la depuración.

Llamada de 3 métodos

El propósito de la fase de llamada al método: determinar la versión del método llamado (qué método), no involucra el proceso de operación específico dentro del método , cuando el programa se está ejecutando, la invocación del método es la operación más común y frecuente.

Todas las llamadas a métodos almacenadas en el archivo de clase son solo referencias de símbolos, que deben determinarse como la dirección de entrada del método en el diseño de la memoria de tiempo de ejecución real durante la carga de la clase o el tiempo de ejecución (equivalente a la referencia directa mencionada anteriormente) .

3.1 Análisis

Los métodos (métodos estáticos y métodos privados) que "conocen en tiempo de compilación e inmutables en tiempo de ejecución" convertirán sus referencias simbólicas en referencias directas (direcciones de entrada) durante la fase de análisis de carga de clases. La invocación de este tipo de método se denomina " Resolución ".

5 proporciona un método en la máquina virtual Java llama a las instrucciones de código de bytes :
invokestatic  : la llamada al método estático
el invokeespecial : instancia de llamada al método constructor, métodos heredados del método privado
invokevirtual : llama a todos los métodos virtuales
invokeinterface : interfaz de llamada Método, un objeto que implementa esta interfaz se determinará en tiempo de ejecución
invocadoinámico : el método al que hace referencia el calificador de puntos se analiza dinámicamente en tiempo de ejecución, y luego se ejecuta el método. La lógica de despacho de los cuatro comandos de invocación anteriores es Se solidifica en la máquina virtual Java , y la lógica de envío de la instrucción dinámica invocada está determinada por el método de guía establecido por el usuario.

3.2 Envío

El proceso de llamada de despacho revelará algunas de las manifestaciones más básicas del polimorfismo, como cómo se implementan la "sobrecarga" y la "sobreescritura" en Java virtual.

1 envío estático

Todas las acciones de envío que se basan en tipos estáticos para localizar la versión de ejecución de un método se denominan envío estático. El envío estático se produce durante la fase de compilación.

La aplicación más típica del envío estático es la sobrecarga de métodos.

package jvm8_3_2;

public class StaticDispatch {
    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human {

    }

    public void sayhello(Human guy) {
        System.out.println("Human guy");

    }

    public void sayhello(Man guy) {
        System.out.println("Man guy");

    }

    public void sayhello(Woman guy) {
        System.out.println("Woman guy");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayhello(man);// Human guy
        staticDispatch.sayhello(woman);// Human guy
    }

}

resultado de la operación:

Chico humano

Chico humano

¿Por qué hay tal resultado?

Human man = new Man (); Entre ellos, Human se llama el tipo estático de la variable (Tipo estático) , y Man se llama el tipo real de la variable (Tipo real) .
La diferencia entre los dos es : el compilador conoce el tipo estático y el tipo real no se determina hasta el tiempo de ejecución.
Cuando se sobrecarga, el tipo estático del parámetro se usa como base de juicio en lugar del tipo real, por lo tanto, en la fase de compilación, el compilador de Javac determinará qué versión sobrecargada usar de acuerdo con el tipo estático del parámetro. Así que elija sayhello (Human) como el destino de la llamada y escriba la referencia simbólica de este método en los parámetros de las dos instrucciones invokevirtual en el método main ().

2 Despacho dinámico

El proceso de envío para determinar la versión de ejecución del método de acuerdo con el tipo real en tiempo de ejecución se denomina envío dinámico. La aplicación más típica es la reescritura de métodos.

package jvm8_3_2;

public class DynamicDisptch {

    static abstract class Human {
        abstract void sayhello();
    }

    static class Man extends Human {

        @Override
        void sayhello() {
            System.out.println("man");
        }

    }

    static class Woman extends Human {

        @Override
        void sayhello() {
            System.out.println("woman");
        }

    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayhello();
        woman.sayhello();
        man = new Woman();
        man.sayhello();
    }

}

resultado de la operación:

hombre

mujer

mujer

3 Envío único y envío múltiple

El receptor del método y los parámetros del método se pueden llamar la cantidad del método. Según la cantidad de cantidades en las que se basan los lotes, la distribución se puede dividir en distribución única y distribución múltiple. La distribución única selecciona el método de destino en función de una cantidad y la distribución múltiple selecciona el método de destino en función de más de una cantidad.

Cuando Java realiza un envío estático, el método de destino debe seleccionarse en función de dos puntos: uno es el tipo estático de la variable y el otro es el tipo del parámetro del método. Debido a que la elección se basa en dos variables, el envío estático del lenguaje Java pertenece al tipo de distribución múltiple.

En el proceso de envío dinámico en tiempo de ejecución, dado que el compilador ha determinado la firma del método de destino (incluidos los parámetros del método), la máquina virtual en tiempo de ejecución solo necesita determinar el tipo real del destinatario del método antes de enviarlo. Debido a que se basa en una cantidad como base para la selección, el envío dinámico del lenguaje Java pertenece al tipo de envío único.

Nota: A partir de JDK1.7, el lenguaje Java sigue siendo un lenguaje de despacho múltiple estático y despacho único dinámico, y puede admitir despacho múltiple dinámico en el futuro.

4 Implementación de despacho dinámico de máquinas virtuales

Debido a que el envío dinámico es una acción muy frecuente, y el envío dinámico necesita buscar el método de destino apropiado en los metadatos del método durante el proceso de selección de la versión del método, la implementación de la máquina virtual generalmente no realiza directamente búsquedas tan frecuentes debido a consideraciones de rendimiento. un método de optimización.

Uno de los métodos de "optimización estable" es crear una tabla de métodos virtuales (Tabla de métodos virtuales, también conocida como vtable) en el área de métodos de la clase. En consecuencia, también hay una Tabla de métodos de interfaz-Tabla de métodos de interfaz, también conocido como itable. Utilice el índice de la tabla de métodos virtuales en lugar de la búsqueda de metadatos para mejorar el rendimiento. El principio es similar a la tabla de funciones virtuales de C ++.

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 dirección en la tabla de métodos virtuales de la subclase es la misma que el método en la clase principal, y ambos apuntan a la entrada de implementación de la clase principal. La tabla de métodos virtuales generalmente se inicializa durante la fase de conexión de la carga de clases.

3.3 Soporte para idiomas escritos dinámicamente

El JDK agregó recientemente la instrucción dinámica invocada para realizar el "lenguaje de tipo dinámico".

La diferencia entre lenguaje estático y lenguaje dinámico:

  • Lenguaje estático (lenguaje fuertemente tipado) : Un
    lenguaje estático es un lenguaje en el que el tipo de datos de una variable se puede determinar en tiempo de compilación. La mayoría de los lenguajes tipados estáticamente requieren que el tipo de datos se declare antes de usar la variable. 
    Por ejemplo: C ++, Java, Delphi, C #, etc.
  • Lenguaje dinámico (lenguaje débilmente tipado)  : un
    lenguaje dinámico es un lenguaje que determina los tipos de datos en tiempo de ejecución. No se requiere una declaración de tipo antes de utilizar la variable. Por lo general, el tipo de la variable es el tipo del valor que se asigna. 
    Por ejemplo, PHP / ASP / Ruby / Python / Perl / ABAP / SQL / JavaScript / Unix Shell y así sucesivamente.
  • Lenguaje de definición fuertemente tipado  : un lenguaje que
    refuerza la definición de tipos de datos. En otras palabras, una vez que a una variable se le asigna un determinado tipo de datos, si no se coacciona, siempre será de este tipo de datos. Por ejemplo: si define una variable entera a, es imposible que el programa trate a como un tipo de cadena. Un lenguaje fuertemente tipado es un lenguaje de tipo seguro.
  • Lenguaje de definición débilmente tipado  : un lenguaje en el que
    se pueden ignorar los tipos de datos. Es contrario a un lenguaje de definición fuertemente tipado, a una variable se le pueden asignar valores de diferentes tipos de datos. Los lenguajes fuertemente tipados pueden ser ligeramente inferiores a los lenguajes débilmente tipados en términos de velocidad, pero el rigor que aportan los lenguajes fuertemente tipados puede evitar efectivamente muchos errores.

4 Motor de ejecución de interpretación de código de bytes basado en pilas

Se ha explicado el contenido de cómo la máquina virtual llama al método, ahora discutiremos cómo la máquina virtual ejecuta las instrucciones de código de bytes en el método.

4.1 Interpretación y ejecución

El lenguaje Java a menudo se posiciona como un  lenguaje de "ejecución interpretada" . En la era JDK1.0 cuando nació Java, esta definición todavía era relativamente precisa. Sin embargo, cuando las máquinas virtuales convencionales incluyen la compilación instantánea, el código en el archivo Class es al final, si será interpretado o ejecutado o compilado y ejecutado es algo que solo la máquina virtual puede juzgar con precisión. Más tarde, Java también desarrolló un compilador que genera directamente código nativo [How to GCJ (GNU Compiler for the Java)], y C / C ++ también apareció a través de una versión de intérprete (como CINT), luego el Decir general "ejecución interpretada" se ha convertido un concepto que casi no tiene significado para todo el lenguaje Java. Solo cuando se determine que el objeto de discusión es una versión específica de implementación de Java y el modo de operación del motor de ejecución, ¿será posible hablar de ejecución interpretada o ejecución de compilación? .

Interpretación y ejecución

En el lenguaje Java, el compilador javac completa el proceso de análisis léxico y análisis gramatical del código del programa en el árbol de sintaxis abstracta, y luego recorre el árbol de sintaxis para generar un flujo de instrucciones de código de bytes lineal, porque esta parte de la acción se realiza fuera la máquina virtual Java, y el intérprete está dentro de la máquina virtual, por lo que la compilación de programas Java se implementa de forma semi-independiente.

4.2 Conjunto de instrucciones basadas en pilas y conjunto de instrucciones basadas en registros

La salida del flujo de instrucciones del compilador de Java es básicamente una arquitectura de conjunto de instrucciones basada en pila (Arquitectura de conjunto de instrucciones, ISA) , que se basa en la pila de operandos para su trabajo . En consecuencia, otra arquitectura de conjunto de instrucciones de uso común es un conjunto de instrucciones basado en registros , que  depende de los registros para funcionar .

Entonces, ¿cuál es la diferencia entre el conjunto de instrucciones basado en pila y el conjunto de instrucciones basado en registro?

Para un ejemplo simple, use estas dos instrucciones para calcular el resultado de 1 + 1. El conjunto de instrucciones basadas en la pila se verá así:
iconst_1

iconst_1

añado

istore_0

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

Si el conjunto de instrucciones se basa en el registro, el programa puede verse así:

mov eax, 1

agregar eax, 1

La instrucción mov establece el valor del registro EAX en 1, y luego la instrucción suma suma 1 al valor y el resultado se almacena en el registro EAX.

La principal ventaja del conjunto de instrucciones basado en pila es que es portátil.Los registros los proporciona directamente el hardware y el programa depende directamente de estos registros de hardware, por lo que está inevitablemente sujeto a las limitaciones del hardware.

El conjunto de instrucciones de la arquitectura de pila tiene algunas otras ventajas, como un código relativamente más compacto y una implementación más sencilla del compilador.
La principal desventaja del conjunto de instrucciones de arquitectura de pila es que la velocidad de ejecución es relativamente lenta.

para resumir

En esta sección, analizamos cómo encontrar el método correcto cuando la máquina virtual ejecuta el código, cómo ejecutar el bytecode en el método y la estructura de memoria involucrada al ejecutar el código.

Supongo que te gusta

Origin blog.csdn.net/wr_java/article/details/115209048
Recomendado
Clasificación