¿El uso de variables finales para en Java mejora la recolección de basura?

86

Hoy mis colegas y yo tenemos una discusión sobre el uso de la finalpalabra clave en Java para mejorar la recolección de basura.

Por ejemplo, si escribe un método como:

public Double doCalc(final Double value)
{
   final Double maxWeight = 1000.0;
   final Double totalWeight = maxWeight * value;
   return totalWeight;  
}

Declarar las variables en el método finalayudaría a la recolección de basura a limpiar la memoria de las variables no utilizadas en el método después de que el método salga.

¿Es esto cierto?

Goran Martinic
fuente
hay dos cosas aquí, en realidad. 1) cuando escribe en un campo local de método y una instancia. Cuando escribes a una instancia, podría haber una ventaja.
Eugene

Respuestas:

86

Aquí hay un ejemplo ligeramente diferente, uno con campos de tipo de referencia final en lugar de variables locales de tipo de valor final:

public class MyClass {

   public final MyOtherObject obj;

}

Cada vez que crea una instancia de MyClass, creará una referencia saliente a una instancia de MyOtherObject, y el GC tendrá que seguir ese enlace para buscar objetos activos.

La JVM utiliza un algoritmo GC de barrido de marca, que tiene que examinar todas las referencias en vivo en las ubicaciones "raíz" de GC (como todos los objetos en la pila de llamadas actual). Cada objeto vivo se "marca" como vivo, y cualquier objeto al que se refiere un objeto vivo también se marca como vivo.

Después de la finalización de la fase de marca, el GC barre el montón, liberando memoria para todos los objetos sin marcar (y compactando la memoria para los objetos vivos restantes).

Además, es importante reconocer que la memoria del montón de Java está dividida en una "generación joven" y una "generación anterior". Todos los objetos se asignan inicialmente a la generación joven (a veces denominada "la guardería"). Dado que la mayoría de los objetos son de corta duración, el GC es más agresivo en cuanto a liberar la basura reciente de la generación joven. Si un objeto sobrevive a un ciclo de recolección de la generación joven, se traslada a la generación anterior (a veces denominada "generación titular"), que se procesa con menos frecuencia.

Entonces, en lo alto de mi cabeza, voy a decir "no, el modificador 'final' no ayuda al GC a reducir su carga de trabajo".

En mi opinión, la mejor estrategia para optimizar la gestión de la memoria en Java es eliminar las referencias falsas lo más rápido posible. Puede hacerlo asignando "nulo" a una referencia de objeto tan pronto como haya terminado de usarlo.

O, mejor aún, minimice el tamaño del alcance de cada declaración. Por ejemplo, si declara un objeto al comienzo de un método de 1000 líneas, y si el objeto permanece vivo hasta el cierre del alcance de ese método (la última llave de cierre), entonces el objeto podría permanecer vivo por mucho más tiempo que en realidad necesario.

Si usa métodos pequeños, con solo una docena de líneas de código, entonces los objetos declarados dentro de ese método caerán fuera del alcance más rápidamente, y el GC podrá hacer la mayor parte de su trabajo dentro de la mucho más eficiente generación joven. No desea que los objetos pasen a la generación anterior a menos que sea absolutamente necesario.

benjismith
fuente
Comida para el pensamiento. Siempre pensé que el código en línea era más rápido, pero si jvm se queda sin memoria, también se ralentizará. hmmmmm ...
WolfmanDragon
1
Solo estoy adivinando aquí ... pero supongo que el compilador JIT puede incorporar valores primitivos finales (no objetos), para aumentos modestos de rendimiento. La inserción de código, por otro lado, es capaz de producir optimizaciones significativas, pero no tiene nada que ver con las variables finales.
benjismith
2
No sería posible asignar nulo a un objeto final ya creado, luego tal vez final en lugar de ayuda, podría hacer las cosas más difíciles
Hernán Eche
1
También se podría usar {} para limitar el alcance en un método grande, en lugar de dividirlo en varios métodos privados que podrían no ser relevantes para otros métodos de clase.
mmm
También puede usar variables locales en lugar de campos cuando sea posible para mejorar la recolección de basura en la memoria y reducir los lazos referenciales.
sivi
37

Declarar una variable local finalno afectará la recolección de basura, solo significa que no puede modificar la variable. Su ejemplo anterior no debería compilarse ya que está modificando la variable totalWeightque se ha marcado final. Por otro lado, declarar una primitiva (en doublelugar de Double) finalpermitirá que esa variable se inserte en el código de llamada, por lo que podría causar alguna mejora de memoria y rendimiento. Esto se usa cuando tienes varios public static final Stringsen una clase.

En general, el compilador y el tiempo de ejecución optimizarán donde sea posible. Es mejor escribir el código apropiadamente y no intentar ser demasiado complicado. Úselo finalcuando no desee que se modifique la variable. Suponga que el compilador realizará las optimizaciones fáciles y, si le preocupa el rendimiento o el uso de la memoria, utilice un generador de perfiles para determinar el problema real.

Aaron
fuente
26

No, enfáticamente no es cierto.

Recuerde que eso finalno significa constante, solo significa que no puede cambiar la referencia.

final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.

Puede haber una pequeña optimización basada en el conocimiento de que la JVM nunca tendrá que modificar la referencia (como no tener que verificar si ha cambiado) pero sería tan pequeña como para no preocuparse.

Final debe considerarse como metadatos útiles para el desarrollador y no como una optimización del compilador.

SCdF
fuente
17

Algunos puntos a aclarar:

  • Anular la referencia no debería ayudar a GC. Si lo hiciera, indicaría que sus variables están fuera de alcance. Una excepción es el caso del nepotismo de objetos.

  • Todavía no hay una asignación en la pila en Java.

  • Declarar una variable final significa que no puede (en condiciones normales) asignar un nuevo valor a esa variable. Dado que final no dice nada sobre el alcance, no dice nada sobre su efecto en GC.

Iglesia
fuente
Hay una asignación en la pila (de primitivas y referencias a objetos en el montón) en java: stackoverflow.com/a/8061692/32453 Java requiere que los objetos en el mismo cierre que una clase / lambda anónima también sean finales, pero cambia que es sólo para reducir la "memoria requerida / marco" / reducir la confusión por lo que no está relacionado con su recogiéndose ...
rogerdpack
11

Bueno, no sé sobre el uso del modificador "final" en este caso, o su efecto en el GC.

Pero puedo decirle esto: su uso de valores en caja en lugar de primitivas (por ejemplo, Double en lugar de double) asignará esos objetos al montón en lugar de a la pila, y producirá basura innecesaria que el GC tendrá que limpiar.

Solo uso primitivas en caja cuando lo requiere una API existente o cuando necesito primitivas que aceptan valores NULL.

benjismith
fuente
1
Tienes razón. Solo necesitaba un ejemplo rápido para explicar mi pregunta.
Goran Martinic
5

Las variables finales no se pueden cambiar después de la asignación inicial (impuesta por el compilador).

Esto no cambia el comportamiento de la recolección de basura como tal. Lo único es que estas variables no se pueden anular cuando no se usan más (lo que puede ayudar a la recolección de basura en situaciones de memoria limitada).

Debe saber que final permite al compilador hacer suposiciones sobre qué optimizar. Insertar código y no incluir el código que se sabe que no es accesible.

final boolean debug = false;

......

if (debug) {
  System.out.println("DEBUG INFO!");
}

El println no se incluirá en el código de bytes.

Thorbjørn Ravn Andersen
fuente
@Eugene Depende de su administrador de seguridad y si el compilador incluyó la variable o no.
Thorbjørn Ravn Andersen
cierto, solo estaba siendo pedante; nada mas; también proporcionó una respuesta también
Eugene
4

Hay un caso de esquina no tan conocido con los recolectores de basura generacionales. (Para una breve descripción lea la respuesta de benjismith para una visión más profunda lea los artículos al final).

La idea en los CG generacionales es que la mayoría de las veces solo deben considerarse las generaciones jóvenes. La ubicación de la raíz se escanea en busca de referencias y luego se escanean los objetos de la generación joven. Durante estos barridos más frecuentes, no se comprueba ningún objeto de la generación anterior.

Ahora, el problema proviene del hecho de que no se permite que un objeto tenga referencias a objetos más jóvenes. Cuando un objeto de larga duración (generación anterior) obtiene una referencia a un nuevo objeto, el recolector de basura debe rastrear explícitamente esa referencia (consulte el artículo de IBM sobre el recolector de JVM de hotspot ), lo que en realidad afecta el rendimiento de GC.

La razón por la que un objeto antiguo no puede referirse a uno más joven es que, dado que el objeto antiguo no se comprueba en las colecciones menores, si la única referencia al objeto se mantiene en el objeto antiguo, no se marcará y estaría mal desasignado durante la etapa de barrido.

Por supuesto, como han señalado muchos, la palabra clave final no afecta realmente al recolector de basura, pero garantiza que la referencia nunca se cambiará a un objeto más joven si este objeto sobrevive a las colecciones menores y llega al montón más antiguo.

Artículos:

IBM sobre recolección de basura: historia , en el hotspot JVM y rendimiento . Es posible que estos ya no sean completamente válidos, ya que datan de 2003/04, pero brindan información fácil de leer sobre los CG.

Recolección de basura Sun on Tuning

David Rodríguez - dribeas
fuente
3

GC actúa sobre referencias inalcanzables. Esto no tiene nada que ver con "final", que es simplemente una afirmación de asignación única. ¿Es posible que algún GC de VM pueda hacer uso de "final"? No veo cómo ni por qué.

dongilmore
fuente
3

finalen variables y parámetros locales no hace ninguna diferencia en los archivos de clase producidos, por lo que no puede afectar el rendimiento en tiempo de ejecución. Si una clase no tiene subclases, HotSpot trata esa clase como si fuera definitiva de todos modos (se puede deshacer más tarde si se carga una clase que rompe esa suposición). Creo que los finalmétodos son muy similares a las clases. finalon static field puede permitir que la variable se interprete como una "constante en tiempo de compilación" y que javac realice la optimización sobre esa base. finalon fields permite a la JVM cierta libertad para ignorar las relaciones de suceso antes de .

Tom Hawtin - tackline
fuente
2

Parece haber muchas respuestas que son conjeturas errantes. La verdad es que no existe un modificador final para las variables locales a nivel de código de bytes. La máquina virtual nunca sabrá que sus variables locales se definieron como finales o no.

La respuesta a su pregunta es un rotundo no.

Matt Quigley
fuente
Eso podría ser cierto, pero el compilador aún puede usar la información final durante el análisis del flujo de datos.
@WernerVanBelle el compilador ya sabe que una variable solo se establece una vez. Tiene que hacer un análisis de flujo de datos para saber que una variable puede ser nula, que no se inicializa antes de su uso, etc. Por lo tanto, las finales locales no ofrecen ninguna información nueva al compilador.
Matt Quigley
No es así. El análisis de flujo de datos puede deducir muchas cosas, pero es posible tener un programa completo de turing dentro de un bloque que establecerá o no una variable local. El compilador no puede saber de antemano si la variable se escribirá y será constante en absoluto. Por lo tanto, el compilador, sin la palabra clave final, no puede garantizar que una variable sea final o no.
@WernerVanBelle Estoy realmente intrigado, ¿puedes dar un ejemplo? No veo cómo puede haber una asignación final a una variable no final que el compilador no conoce. El compilador sabe que si tiene una variable no inicializada, no le permitirá usarla. Si intenta asignar una variable final dentro de un bucle, el compilador no se lo permitirá. ¿Cuál es un ejemplo en el que una variable que PUEDE declararse como final, pero NO lo es, y el compilador no puede garantizar que sea final? Sospecho que cualquier ejemplo sería una variable que no podría declararse como final en primer lugar.
Matt Quigley
Ah, después de volver a leer su comentario, veo que su ejemplo es una variable inicializada dentro de un bloque condicional. Esas variables no pueden ser definitivas en primer lugar, a menos que todas las rutas de condición inicialicen la variable una vez. Entonces, el compilador conoce tales declaraciones; así es como le permite compilar una variable final inicializada en dos lugares diferentes (piense final int x; if (cond) x=1; else x=2;). El compilador, sin la palabra clave final, puede garantizar que una variable sea final o no.
Matt Quigley
1

Todos los métodos y variables se pueden anular por defecto en las subclases. Si queremos evitar que las subclases anulen a los miembros de la superclase, podemos declararlos como finales usando la palabra clave final. Por ejemplo, final int a=10; final void display(){......} hacer un método final asegura que la funcionalidad definida en la superclase nunca será cambiada de todos modos. De manera similar, el valor de una variable final nunca se puede cambiar. Las variables finales se comportan como variables de clase.

Debasish Pakhira
fuente
1

Estrictamente hablando de los campos de instancia , final podría mejorar ligeramente el rendimiento si un GC en particular quiere explotarlo. Cuando GCocurre un concurrente (eso significa que su aplicación aún se está ejecutando, mientras que GC está en progreso ), vea esto para una explicación más amplia , los GC deben emplear ciertas barreras cuando se realizan las escrituras y / o lecturas. El enlace que les di explica bastante eso, pero para hacerlo realmente corto: cuando un GChace un trabajo simultáneo, todos leen y escriben en el montón (mientras ese GC está en progreso), son "interceptados" y aplicados más tarde; para que la fase de GC concurrente pueda terminar su trabajo.

Por finalejemplo, los campos, dado que no se pueden modificar (salvo reflexión), estas barreras pueden omitirse. Y esto no es solo teoría pura.

Shenandoah GClos tiene en la práctica (aunque no por mucho tiempo ), y puede hacer, por ejemplo:

-XX:+UnlockExperimentalVMOptions  
-XX:+UseShenandoahGC  
-XX:+ShenandoahOptimizeInstanceFinals

Y habrá optimizaciones en el algoritmo de GC que lo harán un poco más rápido. Esto se debe a que no habrá barreras que intercepten final, ya que nadie debería modificarlas, nunca. Ni siquiera por reflexión o JNI.

Eugenio
fuente
0

Lo único en lo que puedo pensar es que el compilador podría optimizar las variables finales e incorporarlas como constantes en el código, por lo que terminará sin memoria asignada.

Sijin
fuente
0

Absolutamente, siempre que acorte la vida del objeto, lo que produce un gran beneficio de la gestión de la memoria, recientemente examinamos la funcionalidad de exportación que tiene variables de instancia en una prueba y otra prueba que tiene una variable local de nivel de método. durante la prueba de carga, JVM arroja un error de memoria en la primera prueba y JVM se detuvo. pero en la segunda prueba, se pudo obtener el informe con éxito debido a una mejor gestión de la memoria.

santhosh kumar
fuente
0

La única vez que prefiero declarar las variables locales como finales es cuando:

  • Yo tengo para hacerlos tan definitivo que puedan ser compartidos con alguna clase anónima (por ejemplo: la creación de hilo de utilidad y dejar que se accede a un cierto valor de encerrar método)

  • Yo quiero hacerlos final (por ejemplo: un valor que no debe / no quede anulado por error)

¿Ayudan en la recolección rápida de basura?
AFAIK, un objeto se convierte en candidato para la recolección de GC si no tiene referencias fuertes a él y, en ese caso, tampoco hay garantía de que sean recolectados como basura de inmediato. En general, se dice que una referencia fuerte muere cuando sale del alcance o el usuario la reasigna explícitamente a una referencia nula, por lo tanto, declararla final significa que la referencia continuará existiendo hasta que exista el método (a menos que su alcance se reduzca explícitamente a un bloque interno específico {}) porque no puede reasignar variables finales (es decir, no puede reasignar a nulo). Así que creo que wrt Garbage Collection 'final' puede introducir un posible retraso no deseado, por lo que hay que tener poco cuidado al definir su alcance, ya que controla cuándo se convertirán en candidatos para GC.

sactiw
fuente