memoria manejo library liberar cache borrar java caching java-8 method-reference

manejo - liberar memoria en java



¿El almacenamiento en caché de referencia del método es una buena idea en Java 8? (3)

Debe hacer una distinción entre ejecuciones frecuentes del mismo sitio de llamada , para lambdas sin estado o lambdas con estado completo, y usos frecuentes de una referencia de método al mismo método (por diferentes sitios de llamada).

Mira los siguientes ejemplos:

Runnable r1=null; for(int i=0; i<2; i++) { Runnable r2=System::gc; if(r1==null) r1=r2; else System.out.println(r1==r2? "shared": "unshared"); }

Aquí, el mismo sitio de llamada se ejecuta dos veces, produciendo un lambda sin estado y la implementación actual imprimirá "shared" .

Runnable r1=null; for(int i=0; i<2; i++) { Runnable r2=Runtime.getRuntime()::gc; if(r1==null) r1=r2; else { System.out.println(r1==r2? "shared": "unshared"); System.out.println( r1.getClass()==r2.getClass()? "shared class": "unshared class"); } }

En este segundo ejemplo, el mismo sitio de llamada se ejecuta dos veces, produciendo un lambda que contiene una referencia a una instancia de Runtime y la implementación actual imprimirá "unshared" pero "shared class" .

Runnable r1=System::gc, r2=System::gc; System.out.println(r1==r2? "shared": "unshared"); System.out.println( r1.getClass()==r2.getClass()? "shared class": "unshared class");

Por el contrario, en el último ejemplo hay dos sitios de llamada diferentes que producen una referencia de método equivalente, pero a partir del 1.8.0_05 imprimirá "unshared" y "unshared class" .

Para cada expresión lambda o referencia de método, el compilador emitirá una instrucción invokedynamic que se refiere a un método bootstrap provisto por JRE en la clase LambdaMetafactory y los argumentos estáticos necesarios para producir la clase de implementación lambda deseada. Se deja al JRE real lo que produce la meta fábrica, pero es un comportamiento específico de la instrucción invokedynamic para recordar y reutilizar la instancia de CallSite creada en la primera invocación.

El JRE actual produce un ConstantCallSite contiene un MethodHandle a un objeto constante para lambdas sin estado (y no hay una razón imaginable para hacerlo de manera diferente). Y las referencias de método al método static son siempre sin estado. Por lo tanto, para las lambdas sin estado y los sitios únicos de llamada, la respuesta debe ser: no almacenar en caché, la JVM funcionará y, si no lo hace, debe tener fuertes razones por las que no se debe contrarrestar.

Para las lambdas que tienen parámetros, y this::func es una lambda que tiene una referencia a this instancia, las cosas son un poco diferentes. El JRE tiene permiso para almacenarlos en caché, pero esto implicaría mantener algún tipo de Map entre los valores reales de los parámetros y el lambda resultante, lo que podría ser más costoso que crear simplemente ese ejemplar lambda estructurado simple nuevamente. El JRE actual no almacena en caché las instancias lambda que tienen un estado.

Pero esto no significa que la clase lambda se crea todo el tiempo. Simplemente significa que el sitio de llamada resuelto se comportará como una construcción de objeto ordinaria instanciando la clase lambda que se ha generado en la primera invocación.

Se aplican cosas similares a las referencias de métodos al mismo método de destino creado por diferentes sitios de llamadas. El JRE puede compartir una sola instancia lambda entre ellos, pero en la versión actual no es así, probablemente porque no está claro si el mantenimiento del caché dará resultado. Aquí, incluso las clases generadas pueden diferir.

Así que el almacenamiento en caché como en su ejemplo podría hacer que su programa haga cosas diferentes que sin él. Pero no necesariamente más eficiente. Un objeto en caché no siempre es más eficiente que un objeto temporal. A menos que realmente mida un impacto en el rendimiento causado por una creación lambda, no debe agregar ningún almacenamiento en caché.

Creo que solo hay algunos casos especiales en los que el almacenamiento en caché podría ser útil:

  • estamos hablando de muchos sitios de llamadas diferentes que se refieren al mismo método
  • el lambda se crea en el constructor / clase initialize porque más tarde en el sitio de uso
    • ser llamado por múltiples hilos concurrentemente
    • sufrir el menor rendimiento de la primera invocación

Considera que tengo un código como el siguiente:

class Foo { Y func(X x) {...} void doSomethingWithAFunc(Function<X,Y> f){...} void hotFunction(){ doSomethingWithAFunc(this::func); } }

Supongamos que se llama a hotFunction mucha frecuencia. ¿Sería entonces recomendable guardar en caché this::func , quizás así:

class Foo { Function<X,Y> f = this::func; ... void hotFunction(){ doSomethingWithAFunc(f); } }

En lo que respecta a mi comprensión de las referencias al método java, la máquina virtual crea un objeto de una clase anónima cuando se utiliza una referencia de método. Por lo tanto, el almacenamiento en caché de la referencia crearía ese objeto solo una vez, mientras que el primer enfoque lo crearía en cada llamada de función. ¿Es esto correcto?

¿Deben almacenarse en caché las referencias a los métodos que aparecen en las posiciones activas del código o la máquina virtual puede optimizar esto y hacer que el almacenamiento en caché sea superfluo? ¿Existe una mejor práctica general sobre esto o es altamente específica para la implementación de VM si dicho almacenamiento en caché es de alguna utilidad?


Por lo que entiendo la especificación del lenguaje, permite este tipo de optimización, incluso si cambia el comportamiento observable. Vea las siguientes citas de la sección JSL8 §15.13.3 :

§15.13.3 Evaluación en tiempo de ejecución de referencias de métodos

En tiempo de ejecución, la evaluación de una expresión de referencia de método es similar a la evaluación de una expresión de creación de instancia de clase, en la medida en que la finalización normal produce una referencia a un objeto. [..]

[..] O bien se asigna e inicializa una nueva instancia de una clase con las siguientes propiedades, o se hace referencia a una instancia existente de una clase con las siguientes propiedades.

Una simple prueba muestra que las referencias de métodos para métodos estáticos (can) dan como resultado la misma referencia para cada evaluación. El siguiente programa imprime tres líneas, de las cuales las dos primeras son idénticas:

public class Demo { public static void main(String... args) { foobar(); foobar(); System.out.println((Runnable) Demo::foobar); } public static void foobar() { System.out.println((Runnable) Demo::foobar); } }

No puedo reproducir el mismo efecto para funciones no estáticas. Sin embargo, no he encontrado nada en la especificación del lenguaje que impida esta optimización.

Por lo tanto, siempre que no haya un análisis de rendimiento para determinar el valor de esta optimización manual, le recomiendo encarecidamente que no lo haga. El almacenamiento en caché afecta la legibilidad del código, y no está claro si tiene algún valor. La optimización prematura es la fuente de todos los males.


Una situación en la que es un buen ideal, lamentablemente, es si la lambda se pasa como un oyente que desea eliminar en algún momento en el futuro. La referencia almacenada en caché será necesaria ya que se pasa a otra esta :: método de referencia no se verá como el mismo objeto en la eliminación y el original no se eliminará. Por ejemplo:

public class Example { public void main( String[] args ) { new SingleChangeListenerFail().listenForASingleChange(); SingleChangeListenerFail.observableValue.set( "Here be a change." ); SingleChangeListenerFail.observableValue.set( "Here be another change that you probably don''t want." ); new SingleChangeListenerCorrect().listenForASingleChange(); SingleChangeListenerCorrect.observableValue.set( "Here be a change." ); SingleChangeListenerCorrect.observableValue.set( "Here be another change but you''ll never know." ); } static class SingleChangeListenerFail { static SimpleStringProperty observableValue = new SimpleStringProperty(); public void listenForASingleChange() { observableValue.addListener(this::changed); } private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue ) { System.out.println( "New Value: " + newValue ); observableValue.removeListener(this::changed); } } static class SingleChangeListenerCorrect { static SimpleStringProperty observableValue = new SimpleStringProperty(); ChangeListener<String> lambdaRef = this::changed; public void listenForASingleChange() { observableValue.addListener(lambdaRef); } private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue ) { System.out.println( "New Value: " + newValue ); observableValue.removeListener(lambdaRef); } } }

Hubiera sido bueno no necesitar lambdaRef en este caso.