Notas de lectura "Comprensión profunda de la máquina virtual Java" (7): motor de ejecución de código de bytes de máquina virtual (a continuación)

Tabla de contenido

1. Compatibilidad con el lenguaje de tipo dinámico Java

1.1 MethodHandle

1.2 La diferencia entre MethodHandle y Reflection

1.3 instrucción dinámica invocada

2. Motor de ejecución de interpretación de códigos de bytes basado en pilas

2.1 Basado en pila y basado en registro

2.2 El proceso de ejecución del intérprete basado en pila

Tres, resumen


1. Compatibilidad con el lenguaje de tipo dinámico Java

La característica clave de un lenguaje tipado dinámicamente es que el proceso principal de verificación de tipos se realiza en tiempo de ejecución y no en tiempo de compilación. Hay muchos lenguajes que cumplen con esta función, como JavaScript, Python, etc., en contraste, los lenguajes Que realizan la verificación de tipos en tiempo de compilación (como C ++ / Java, etc.) es el lenguaje de tipado estático más utilizado .

Por ejemplo, el siguiente código:

obj.println("hello world");

Suponiendo que esta línea de código está en el lenguaje Java, y el tipo estático de la variable obj es java.io.PrintStream, entonces el tipo real de la variable obj debe ser una subclase de PrintStream (implementando la interfaz PrintStream) para ser legal , de lo contrario, incluso si obj lo hace. Existe un método println (String) legal, pero no tiene relación de herencia con la interfaz PrintStream, y el código no se puede ejecutar porque la verificación de tipo es ilegal.

El mismo código en JavaScript es diferente No importa qué tipo de obj sea, siempre que la definición de este tipo incluya el método println (String), la llamada al método puede tener éxito.

La razón de esta diferencia es que el lenguaje Java genera una referencia simbólica completa del método println (String) en tiempo de compilación y la almacena en el archivo de clase como un parámetro de la instrucción de llamada al método, como el siguiente código:

invokevirtual #4//Method java/io/printStream.println:(Ljava/lang/String;)V

Esta referencia simbólica contiene información como el tipo específico en el que se define el método, el nombre del método, el orden de los parámetros, el tipo de parámetro y el valor de retorno del método. A través de esta referencia simbólica, la máquina virtual puede traducir la referencia directa de este método. . En un lenguaje de tipado dinámico como JavaScript, la variable obj en sí no tiene tipo y el valor de la variable obj sí tiene un tipo . En el momento de la compilación, solo se pueden determinar como máximo el nombre del método, los parámetros y el valor de retorno, en lugar de determinar el método específico.

Los lenguajes tipados estáticamente determinan los tipos en tiempo de compilación, por lo que el compilador puede proporcionar una verificación de tipos rigurosa, lo que conduce a la estabilidad ; mientras que los lenguajes dinámicos determinan los tipos en tiempo de ejecución, que es más flexible , y al implementar funciones, es más claro y Más claro que los lenguajes escritos estáticamente. Conciso, el código no parecerá tan "hinchado".

Dado que el primer parámetro de las instrucciones invokevirtual, invokeespecial, invokestatic e invokeinterface para estos métodos es la referencia simbólica del método llamado, y la referencia simbólica se genera en tiempo de compilación, y el lenguaje de tipo dinámico solo puede determinar el tipo de receptor del método en Por lo tanto, se agregó un nuevo comando dinámico invocado en JDK1.7 para brindar soporte.

1.1 MethodHandle

JDK1.7 proporciona un nuevo mecanismo para determinar dinámicamente el método de destino, llamado MethodHandle , además del método anterior que simplemente se basó en referencias simbólicas para determinar el método de destino que se llamará . Su función es básicamente encontrar una firma de método que coincida con el método en una clase, y el resultado está representado por MethodHandle, y luego se puede llamar al método a través de MethodHandle. De esta manera, el código se utiliza para simular el proceso de envío de máquinas virtuales y métodos de búsqueda, lo que otorga a los programadores un mayor grado de libertad.

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class Main {
    static class ClassA {
        public void println(String arg) {
            System.out.println(arg);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = new ClassA();
        getMethodHandle(obj).invoke("hello MethodHandle");
    }

    private static MethodHandle getMethodHandle(Object receiver) throws Exception {
        //定义一个MethodType
        //第一个参数(void.class)是方法的返回类型,后面的参数(String.class)是方法的参数
        MethodType methodType = MethodType.methodType(void.class, String.class);
        //在receiver.class中寻找方法
        //并且通过bindTo将该方法的接收者(也就是this)传递给它
        return MethodHandles.lookup()
                .findVirtual(receiver.getClass(), "println", methodType)
                .bindTo(receiver);
    }
}

El código de muestra anterior usa invokeVirtual, que simula el proceso de ejecución de la instrucción invokevirtual. Otros incluyen findStatic, findSpecial, etc.

1.2 La diferencia entre MethodHandle y Reflection

  • En esencia, los mecanismos Reflection y Method simulan llamadas a métodos, pero Reflection simula llamadas a métodos en el nivel de código Java ; los tres métodos en MethodHandles.lookup: findStatic (), findVirtual () y findSpecial () son solo para Corresponding to el comportamiento de verificación del permiso de ejecución de las diversas instrucciones de código de bytes de invokestatic, invokevirtual & invokeinterface e invokeespecial, y estos detalles de bajo nivel no necesitan preocuparse cuando se utiliza la API de Reflection.

  • La reflexión es un peso pesado . El objeto Mehod contiene firmas de métodos, descriptores y representaciones del lado de Java de varias propiedades en la tabla de atributos del método. También contiene información de tiempo de ejecución, como permisos de ejecución. MethodHandle solo contiene información relacionada con la ejecución del método, como el nombre del método y los parámetros, y es relativamente ligero .

  • Debido a que MethodHandle es una simulación de instrucciones de ejecución de métodos de código de bytes, en teoría, varias optimizaciones (como la inserción de métodos) realizadas por máquinas virtuales en esta área también deberían estar respaldadas por ideas similares en MethodHandle, y mediante Reflection To llamar al método no funcionará.

  • El objetivo de diseño de Reflection es servir solo el lenguaje Java , mientras que MethodHandle está diseñado para servir todos los lenguajes en la máquina virtual Java .

1.3 instrucción dinámica invocada

La función de la instrucción dinámica invocada y el mecanismo MethodHandle es la misma, ambos son para resolver el problema de que las reglas de despacho de instrucciones de llamada de cuatro métodos originales están arregladas en la máquina virtual, y para transferir la decisión sobre cómo encontrar el método de destino del máquina virtual al código de usuario específico. Entre ellos, permiten a los usuarios tener un mayor grado de libertad. El pensamiento de los dos también es análogo, pero uno usa el código Java de nivel superior y la API para lograr, y el otro usa el código de bytes y otros atributos y constantes en la clase para completar.

Invokedynamic cada posición contiene instrucciones se denominan " sitio de llamada dinámica " ( sitio de llamada dinámica ), esta instrucción es la primera instrucción que ya no es representativa del método de referencia simbólico constante CONSTANT_Methodref_info, pero nuevos entrantes en un JDK 1.7 la constante CONSTANT_InvokeDynamic_info , la constante de esta nueva información se puede obtener en 3: método de orientación , método del tipo y nombre . El método de arranque es un parámetro fijo y el valor de retorno es un objeto java.lang.invoke.CallSite, que representa la llamada al método de destino que se ejecutará. A través de CONSTANT_InvokeDynamic_info, el método de arranque se puede encontrar y ejecutar, obteniendo así un objeto CallSite y finalmente llamando al método de destino para que se ejecute.

La mayor diferencia entre invokeynamic y las cuatro instrucciones invoke * anteriores es que su lógica de despacho no está determinada por la máquina virtual, sino por el programador. El usuario de esta instrucción no es el lenguaje Java, sino un lenguaje dinámico en otras máquinas virtuales Java, por lo tanto, el compilador basado en lenguaje Java javac no puede generar código de bytes con la instrucción dinámica invocada.

Al final de esta sección, se da una pregunta muy interesante en el libro: ¿Cómo llamar al método de anulación de la clase de abuelos en la subclase?

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class Main {
    static class GrandFather {
        void thinking() {
            System.out.println("i am grandfather");
        }
    }

    static class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }

    static class Son extends Father {
        void thinking() {
           //只完善这个方法的代码,实现调用祖父类的thinking()方法,打印"i am grandfather"
        }
    }

    public static void main(String[] args) throws Throwable {
        Son son = new Son();
        son.thinking();
    }

}

Nota: Por supuesto, no permitimos que se complete: new GrandFather (). Thinking ();

Antes de JDK1.7, no teníamos forma, porque el despacho dinámico de la instrucción virtual invoke utiliza el tipo real del receptor. Esta lógica se solidifica en la máquina virtual, pero no se puede obtener un tipo real en el método de pensamiento de la clase Son. Consulte al objeto de GrandFather (a menos que volvamos a crear una instancia). Pero después de JDK1.7, puede usar MethodHandle:

static class Son extends Father {
        void thinking() {
            try {
                MethodType methodType = MethodType.methodType(void.class);
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                ((MethodHandles.Lookup) IMPL_LOOKUP.get(null)).findSpecial(GrandFather.class, "thinking", methodType, Father.class)
                        .invoke(this);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

Usamos MethodHandle para simular la instrucción de invocación especial, encontrar el método de pensamiento de GrandFaher.class de acuerdo con nuestros propios deseos y completar nuestra propia lógica de envío.

Nota: La solución dada en el libro es:

MethodType methodType = MethodType.methodType(void.class);
MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", methodType, this.getClass())
    .bindTo(this)
    .invoke();

Sin embargo, se ha verificado que el código no puede lograr el efecto esperado bajo JDK1.7 y 1.8. El bloguero encontró una manera de usar IMPL_LOOKUP mirando el código fuente de Lookup. Para más detalles, vea el área de comentarios ~

2. Motor de ejecución de interpretación de códigos de bytes basado en pilas

En el lenguaje Java, el compilador javac completa el proceso de análisis léxico -> análisis gramatical -> árbol de sintaxis abstracta -> atravesando el árbol de sintaxis para generar un flujo de instrucciones de código de bytes lineal , y el intérprete está dentro de la máquina virtual.

2.1 Basado en pila y basado en registro

La salida del flujo de instrucciones del compilador de Java es básicamente una arquitectura de conjunto de instrucciones basada en pila La mayoría de las instrucciones en el flujo de instrucciones son instrucciones de dirección cero y dependen de la pila de operandos para funcionar. Por el contrario, otro conjunto de arquitectura de conjunto de instrucciones es un conjunto de instrucciones basado en registros, y estas instrucciones se basan en registros para funcionar.

Si desea calcular el resultado de "1 + 1", el flujo de instrucciones basado en la pila se vería así:

iconst_1 //int类型的1入栈
iconst_1 //int类型的1入栈
iadd //栈顶两个int类型出栈,相加,把结果入栈
istore_0 //将栈顶的值出栈放到局部变量表的第0位置的slot中

Si se basa en registros, podría verse así:

mov eax,1 //把eax寄存器的值设为1
add eax,1 //把eax寄存器的值加1,结果保存在eax寄存器

El conjunto de instrucciones basado en pila es portátil y los registros son proporcionados directa o indirectamente por el hardware.Los programas que dependen de estos registros de hardware están sujetos a restricciones de hardware; sin embargo, el conjunto de instrucciones basado en pila requiere más instrucciones para completar la misma función que La arquitectura del registro. Muchos, y la pila se implementa en la memoria. El acceso frecuente a la pila significa el acceso frecuente a la memoria. En relación con el procesador, la memoria es siempre el cuello de botella de la velocidad de ejecución. Debido al número de instrucciones y al acceso a la memoria , la velocidad de ejecución del conjunto de instrucciones de la arquitectura de la pila es relativamente lenta. Los conjuntos de instrucciones de todas las máquinas físicas convencionales se basan en registros.

2.2 El proceso de ejecución del intérprete basado en pila

Aquí se usa el mismo código de muestra del libro, pero por conveniencia, no dibujaré la imagen. En su lugar, anotaré el estado de la pila de operandos y la tabla de variables locales con texto después de cada instrucción. El código Java es el siguiente:

public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }

Después de la compilación, vea las instrucciones de código de bytes a través de javap (los cambios de la pila de operandos y la tabla de variables locales están en las observaciones, donde se describe la pila, el lado derecho es la dirección de la parte superior de la pila ):

public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100  //将100入栈。栈:100;变量表:0=this
         2: istore_1           //将100出栈,存放到局部变量表第1个slot。栈:空;变量表:0=this,1=100
         3: sipush        200  //将200入栈。栈:200;变量表:0=this,1=100
         6: istore_2           //将200出栈,存放到局部变量表第2个slot。栈:空;变量表:0=this,1=100,2=200
         7: sipush        300  //将300入栈。栈:300;变量表:0=this,1=100,2=200
        10: istore_3           //将300出栈,存放到局部变量表第3个slot。栈:空;变量表:0=this,1=100,2=200,3=300
        11: iload_1            //将局部变量表中第1个slot整型值入栈。栈:100;变量表:0=this,1=100,2=200,3=300
        12: iload_2            //将局部变量表中第2个slot整型值入栈。栈:100,200;变量表:0=this,1=100,2=200,3=300
        13: iadd               //将栈顶两个元素出栈做整型加法,然后把结果入栈。栈:300;变量表:0=this,1=100,2=200,3=300
        14: iload_3            //将局部变量表中第3个slot整型值入栈。栈:300,300;变量表:0=this,1=100,2=200,3=300
        15: imul               //将栈顶两个元素出栈做整型乘法,然后把结果入栈。栈:90000;变量表:0=this,1=100,2=200,3=300
        16: ireturn            //结束方法执行,将栈顶整型值返回给方法调用者
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 7
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   Lcom/demo/Main;
            3      14     1     a   I
            7      10     2     b   I
           11       6     3     c   I

Se puede ver en lo anterior que este código requiere una pila de operandos con una profundidad de 2 (refiérase a la profundidad máxima de la pila durante el proceso pop / push), y 4 ranuras de espacio variable local (this, a, b, C)

Para conocer el significado de cada instrucción, puede consultar el documento oficial: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

El proceso de ejecución anterior es solo un modelo conceptual. La máquina virtual eventualmente optimizará el proceso de ejecución para mejorar el rendimiento. El proceso de operación real puede ser muy diferente del modelo conceptual. Debido a que tanto el analizador como el compilador justo a tiempo de la máquina virtual optimizan el código de bytes de entrada, por ejemplo, en la máquina virtual HotSpot, hay muchas instrucciones de código de bytes no estándar que comienzan con "fast_" para fusionar y reemplazar los bytes de entrada. El código para mejorar el rendimiento de interpretación y ejecución, y los métodos de optimización del compilador just-in-time son más diversos (se presentarán en el siguiente capítulo).

Tres, resumen

Aunque gran parte del contenido descrito anteriormente se basa en el modelo conceptual de la máquina virtual Java, habrá una cierta brecha con la situación real, pero esto no obstaculiza nuestra comprensión del principio de la máquina virtual.

Supongo que te gusta

Origin blog.csdn.net/huangzhilin2015/article/details/114467776
Recomendado
Clasificación