variable type tutorial parameter funciones ejemplos calculo java lambda jvm java-8 bytecode

type - lambda stream java



¿Por qué se invocan las lambdas de Java 8 usando invokedynamic? (3)

La instrucción invokedynamic se utiliza para ayudar a la VM a determinar la referencia del método en tiempo de ejecución en lugar de cablearla en tiempo de compilación.

Esto es útil con lenguajes dinámicos donde el método exacto y los tipos de argumento no se conocen hasta el tiempo de ejecución. Pero ese no es el caso con las lambdas de Java. Se traducen a un método estático con argumentos bien definidos. Y este método se puede invocar usando invokestatic .

Entonces, ¿cuál es la necesidad de invokedynamic para lambdas, especialmente cuando hay un golpe de rendimiento?


Brain Goetz explicó las razones de la estrategia de traducción lambda en uno de sus documentos que, lamentablemente, ahora no parecen estar disponibles. Afortunadamente guardé una copia:

Estrategia de traducción

Hay varias formas en que podríamos representar una expresión lambda en bytecode, como clases internas, identificadores de métodos, proxys dinámicos y otros. Cada uno de estos enfoques tiene pros y contras. Al seleccionar una estrategia, hay dos objetivos en competencia: maximizar la flexibilidad para la optimización futura al no comprometerse con una estrategia específica, frente a proporcionar estabilidad en la representación del archivo de clase. Podemos lograr ambos objetivos mediante el uso de la función dinámica invocada de JSR 292 para separar la representación binaria de la creación lambda en el código de bytes de la mecánica de evaluación de la expresión lambda en tiempo de ejecución. En lugar de generar bytecode para crear el objeto que implementa la expresión lambda (como llamar a un constructor para una clase interna), describimos una receta para construir el lambda y delegamos la construcción real al tiempo de ejecución del lenguaje. Esa receta está codificada en las listas de argumentos estáticos y dinámicos de una instrucción invocada dinámica.

El uso de invocados dinámicos nos permite diferir la selección de una estrategia de traducción hasta el tiempo de ejecución. La implementación en tiempo de ejecución es libre de seleccionar una estrategia dinámicamente para evaluar la expresión lambda. La opción de implementación en tiempo de ejecución está oculta detrás de una API estandarizada (es decir, parte de la especificación de la plataforma) para la construcción lambda, de modo que el compilador estático pueda emitir llamadas a esta API, y las implementaciones de JRE pueden elegir su estrategia de implementación preferida. La mecánica dinámica invocada permite que esto se realice sin los costos de rendimiento que este enfoque de enlace tardío podría imponer de otra manera.

Cuando el compilador encuentra una expresión lambda, primero baja (desugará) el cuerpo lambda a un método cuya lista de argumentos y tipo de retorno coincidan con los de la expresión lambda, posiblemente con algunos argumentos adicionales (para valores capturados del ámbito léxico, si los hay). ) En el punto en el que se capturaría la expresión lambda, genera un sitio de llamada dinámico invocado, que, cuando se invoca, devuelve una instancia de la interfaz funcional a la que se está convirtiendo el lambda. Este sitio de llamada se llama fábrica de lambda para una determinada lambda. Los argumentos dinámicos para la fábrica lambda son los valores capturados del ámbito léxico. El método bootstrap de la fábrica lambda es un método estandarizado en la biblioteca de tiempo de ejecución del lenguaje Java, llamado metafactory lambda. Los argumentos de arranque estáticos capturan información conocida sobre el lambda en tiempo de compilación (la interfaz funcional a la que se convertirá, un identificador de método para el cuerpo lambda desugared, información sobre si el tipo SAM es serializable, etc.)

Las referencias de método se tratan de la misma manera que las expresiones lambda, excepto que la mayoría de las referencias de método no necesitan ser eliminadas en un nuevo método; simplemente podemos cargar un identificador de método constante para el método referenciado y pasarlo a la metafactory.

Entonces, la idea aquí parecía ser encapsular la estrategia de traducción y no comprometerse a una forma particular de hacer las cosas ocultando esos detalles. En el futuro, cuando se hayan resuelto los tipos de borrado y falta de valor, y tal vez Java admita tipos de funciones reales, podrían ir allí y cambiar esa estrategia por otra sin causar ningún problema en el código de los usuarios.


La implementación lambda actual de Java 8 es una decisión compuesta:

    1. Compile la expresión lambda al método estático en la clase adjunta; en lugar de compilar lambdas para separar archivos de clase interna (Scala compila de esta manera, por lo que hay muchos archivos de clase $$$)
    1. Introduzca un grupo constante: BootstrapMethods, que ajusta la invocación del método estático al objeto del sitio de llamadas (se puede almacenar en caché para su uso posterior)

Entonces para responder tu pregunta,

    1. La implementación actual de lambda usando invokedynamic es un poco más rápida que la forma de clase interna separada, porque no es necesario cargar estos archivos de clase interna, sino crear el byte de clase interna [] sobre la marcha (para satisfacer, por ejemplo, la interfaz de función), y en caché para su uso posterior.
    1. El equipo de JVM aún puede optar por generar archivos de clase interna separados (por referencia a los métodos estáticos de la clase adjunta), es flexible

Las lambdas no se invocan usando invocacional invokedynamic , su representación de objeto se crea usando invokedynamic , la invocación real es una invocación invokevirtual o invokeinterface invocación regular.

Por ejemplo:

// creates an instance of (a subclass of) Consumer // with invokedynamic to java.lang.invoke.LambdaMetafactory something(x -> System.out.println(x)); void something(Consumer<String> consumer) { // invokeinterface consumer.accept("hello"); }

Cualquier lambda tiene que convertirse en una instancia de alguna clase base o interfaz. Esa instancia a veces contendrá una copia de las variables capturadas del método original y, a veces, un puntero al objeto principal. Esto se puede implementar como una clase anónima.

Por qué invocar dinámicamente

La respuesta corta es: generar código en tiempo de ejecución.

Los mantenedores de Java optaron por generar la clase de implementación en tiempo de ejecución. Esto se hace llamando a java.lang.invoke.LambdaMetafactory.metafactory . Dado que los argumentos para esa llamada (tipo de retorno, interfaz y parámetros capturados) pueden cambiar, esto requiere invokedynamic .

El uso de invokedynamic para construir la clase anónima en tiempo de ejecución permite que la JVM genere ese invokedynamic de invokedynamic de clase en tiempo de ejecución. Las llamadas posteriores a la misma declaración utilizan una versión en caché. La otra razón para usar invokedynamic es poder cambiar la estrategia de implementación en el futuro sin tener que cambiar el código ya compilado.

El camino no tomado

La otra opción sería el compilador creando una clase interna para cada instancia lambda, equivalente a traducir el código anterior a:

something(new Consumer() { public void accept(x) { // call to a generated method in the base class ImplementingClass.this.lambda$1(x); // or repeating the code (awful as it would require generating accesors): System.out.println(x); } );

Esto requiere crear clases en tiempo de compilación y tener que cargarlas durante el tiempo de ejecución. La forma en que jvm funciona esas clases residiría en el mismo directorio que la clase original. Y la primera vez que ejecuta la instrucción que usa esa lambda, esa clase anónima tendría que cargarse e inicializarse.

Sobre el rendimiento

La primera llamada a invokedynamic activará la generación de clase anónima. Luego, el código de operación invokedynamic se reemplaza con un code que es equivalente en rendimiento a la escritura manual de la instancia anónima.