¿Anular Object.finalize () es realmente malo?

34

Los dos argumentos principales contra la anulación Object.finalize()son:

  1. No puedes decidir cuándo se llama.

  2. Es posible que no se llame en absoluto.

Si entiendo esto correctamente, no creo que sean razones suficientes para odiar Object.finalize()tanto.

  1. Depende de la implementación de la VM y del GC determinar cuándo es el momento adecuado para desasignar un objeto, no el desarrollador. ¿Por qué es importante decidir cuándo Object.finalize()se llama?

  2. Normalmente, y corríjame si me equivoco, el único momento en Object.finalize()que no se llamaría es cuando la aplicación se terminara antes de que el GC tuviera la oportunidad de ejecutarse. Sin embargo, el objeto se desasignó de todos modos cuando el proceso de la aplicación terminó con él. Entonces Object.finalize()no me llamaron porque no era necesario llamarlo. ¿Por qué le importaría al desarrollador?

Cada vez que uso objetos que tengo que cerrar manualmente (como los identificadores de archivos y las conexiones), me siento muy frustrado. Tengo que estar constantemente comprobando si un objeto tiene una implementación close(), y estoy seguro de que he perdido algunas llamadas en algunos puntos en el pasado. ¿Por qué no es más simple y seguro dejar que la VM y el GC eliminen estos objetos mediante la close()implementación Object.finalize()?

AxiomaticNexus
fuente
1
También tenga en cuenta: como muchas API de la era de Java 1.0, la semántica de subprocesos finalize()está un poco desordenada. Si alguna vez lo implementa, asegúrese de que sea seguro para subprocesos con respecto a todos los demás métodos en el mismo objeto.
billc.cn
2
Cuando escuchas a las personas decir que los finalizadores son malos, no significan que tu programa dejará de funcionar si los tienes; significan que toda la idea de finalización es bastante inútil.
user253751
1
+1 a esta pregunta La mayoría de las respuestas a continuación indican que los recursos, como los descriptores de archivo, son limitados y que deben recopilarse manualmente. Lo mismo sucedió / es cierto con respecto a la memoria, por lo que si aceptamos algún retraso en la recopilación de memoria, ¿por qué no aceptarlo para descriptores de archivos y / u otros recursos?
mbonnin
Al abordar su último párrafo, puede dejar que Java se encargue de cerrar cosas como identificadores de archivos y conexiones con muy poco esfuerzo de su parte. Use el bloque de prueba con recursos: ya se menciona algunas veces en las respuestas y comentarios, pero creo que vale la pena ponerlo aquí. El tutorial de Oracle para esto se encuentra en docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Respuestas:

45

En mi experiencia, hay una y solo una razón para anular Object.finalize(), pero es una muy buena razón :

Para colocar el código de registro de errores en el finalize()que le notifica si alguna vez olvida invocar close().

El análisis estático solo puede detectar omisiones en escenarios de uso triviales, y las advertencias del compilador mencionadas en otra respuesta tienen una visión tan simplista de las cosas que realmente tiene que deshabilitarlas para hacer algo no trivial. (Tengo muchas más advertencias habilitadas que cualquier otro programador que conozca o haya escuchado, pero no tengo habilitadas las advertencias estúpidas).

La finalización puede parecer un buen mecanismo para asegurarse de que los recursos no se disuelvan, pero la mayoría de las personas lo ven de una manera completamente incorrecta: piensan que es un mecanismo alternativo alternativo, una salvaguardia de "segunda oportunidad" que salvará automáticamente día al deshacerse de los recursos que olvidaron. Esto está muy mal . Debe haber una sola forma de hacer cualquier cosa: o siempre cierras todo o la finalización siempre cierra todo. Pero como la finalización no es confiable, la finalización no puede serlo.

Por lo tanto, existe este esquema que llamo Eliminación obligatoria , y estipula que el programador es responsable de cerrar siempre explícitamente todo lo que implementa Closeableo AutoCloseable. (La declaración de prueba con recursos todavía cuenta como cierre explícito). Por supuesto, el programador puede olvidar, así que ahí es donde entra en juego la finalización, pero no como un hada mágica que mágicamente hará las cosas bien al final: si la finalización descubre que close()no fue invocado, nointente invocarlo, precisamente porque (con certeza matemática) habrá hordas de programadores n00b que dependerán de él para hacer el trabajo que eran demasiado vagos o demasiado distraídos para hacer. Entonces, con la disposición obligatoria, cuando la finalización descubre que close()no se invocó, registra un mensaje de error rojo brillante, diciéndole al programador con mayúsculas grandes y gruesas que arreglen sus cosas.

Como beneficio adicional, se rumorea que "la JVM ignorará un método trivial finalize () (p. Ej., Uno que simplemente regresa sin hacer nada, como el definido en la clase Object)", por lo que con la eliminación obligatoria puede evitar toda finalización sobrecarga en todo su sistema ( vea la respuesta de alip para obtener información sobre cuán terrible es esta sobrecarga) codificando su finalize()método de esta manera:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

La idea detrás de esto es que Global.DEBUGes una static finalvariable cuyo valor se conoce en el momento de la compilación, por lo que si es falseasí, el compilador no emitirá ningún código para toda la ifdeclaración, lo que lo convertirá en un finalizador trivial (vacío), que a su vez significa que su clase será tratada como si no tuviera un finalizador. (En C # esto se haría con un buen #if DEBUGbloque, pero qué podemos hacer, esto es Java, donde pagamos aparente simplicidad en el código con una sobrecarga adicional en el cerebro).

Más información sobre la eliminación obligatoria, con una discusión adicional sobre la eliminación de recursos en dot Net, aquí: michael.gr: eliminación obligatoria frente a la abominación de "eliminación y eliminación"

Mike Nakis
fuente
2
@MikeNakis No lo olvide, Closeable se define como no hacer nada si se llama por segunda vez: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Admito que a veces he registrado una advertencia cuando mis clases están cerradas dos veces, pero técnicamente ni siquiera se supone que hagas eso. Sin embargo, técnicamente, llamar a .close () en Closable más de una vez es perfectamente válido.
Patrick M
1
@ usr todo se reduce a si confía en sus pruebas o no confía en sus pruebas. Si no confía en sus pruebas, claro, continúe y sufra la sobrecarga de finalización también close() , por si acaso. Creo que si no se puede confiar en mis pruebas, será mejor que no lance el sistema a producción.
Mike Nakis
3
@Mike, para que la if( Global.DEBUG && ...construcción funcione de modo que la JVM ignore el finalize()método como trivial, Global.DEBUGdebe establecerse en el momento de la compilación (en lugar de inyectado, etc.), de modo que lo que sigue será un código muerto. ¡La llamada al super.finalize()bloque if también es suficiente para que la JVM lo trate como no trivial (independientemente del valor de Global.DEBUG, al menos en HotSpot 1.8), incluso si la superclase #finalize()es trivial!
alip
1
@ Mike, me temo que ese es el caso. Lo probé con (una versión ligeramente modificada) de la prueba en el artículo al que se vinculó , y la salida detallada del GC (junto con un rendimiento sorprendentemente pobre) confirman que los objetos se copian en el espacio sobreviviente / generación anterior y necesitan GC de montón completo para obtener deshacerse de
alip
1
Lo que a menudo se pasa por alto es el riesgo de una colección de objetos anterior a la esperada, lo que hace que liberar recursos en el finalizador sea una acción muy peligrosa. Antes de Java 9, la única forma de asegurarse de que un finalizador no cierre el recurso mientras todavía está en uso, es sincronizar en el objeto, tanto en el finalizador como en los métodos que usan el recurso. Por eso funciona java.io. Si ese tipo de seguridad de hilo no estaba en la lista de deseos, se suma a la sobrecarga inducida por finalize()...
Holger
28

Cada vez que uso objetos que tengo que cerrar manualmente (como los identificadores de archivos y las conexiones), me siento muy frustrado. [...] ¿Por qué no es más simple y seguro dejar que la VM y el GC eliminen estos objetos mediante la close()implementación Object.finalize()?

Debido a que los identificadores y las conexiones de los archivos (es decir, los descriptores de archivos en los sistemas Linux y POSIX) son un recurso bastante escaso (es posible que esté limitado a 256 de ellos en algunos sistemas, o a 16384 en otros; ver setrlimit (2) ). No hay garantía de que se llame al GC con la suficiente frecuencia (o en el momento adecuado) para evitar agotar estos recursos limitados. Y si el GC no se llama lo suficiente (o la finalización no se ejecuta en el momento adecuado), alcanzará ese límite (posiblemente bajo).

La finalización es una cuestión de "mejor esfuerzo" en la JVM. Es posible que no se llame, o que se llame bastante tarde ... En particular, si tiene mucha RAM o si su programa no asigna muchos objetos (o si la mayoría de ellos mueren antes de ser enviados a algunos lo suficientemente mayores) generación por un GC generacional de copia), el GC podría llamarse muy raramente, y las finalizaciones podrían no ejecutarse muy a menudo (o incluso podrían no ejecutarse en absoluto).

Entonces closelos descriptores de archivo explícitamente, si es posible. Si tiene miedo de filtrarlos, use la finalización como una medida adicional, no como la principal.

Basile Starynkevitch
fuente
77
Agregaría que cerrar una secuencia a un archivo o socket normalmente lo vacía. Dejando los flujos abiertos innecesariamente aumenta el riesgo de pérdida de datos si se va la luz, cae una conexión (esto es también un riesgo para los archivos accedidos a través de una red), etc.
2
En realidad, aunque el número de descriptores de archivos permitidos puede ser bajo, ese no es el punto realmente difícil, porque uno podría usar eso como una señal para el GC al menos. Lo realmente problemático es a) completamente intransparente para el GC cuántos recursos no administrados por GC dependen de él, yb) muchos de esos recursos son únicos, por lo que otros podrían ser bloqueados o denegados.
Deduplicador
2
Y si deja un archivo abierto, podría interferir con el uso de otra persona. (Visor XPS de Windows 8, ¡te estoy mirando!)
Loren Pechtel
2
"Si tiene miedo de filtrar [descriptores de archivo], utilice la finalización como una medida adicional, no como la principal". Esta declaración me suena sospechosa. Si diseña bien su código, ¿debería realmente introducir código redundante que extienda la limpieza en varios lugares?
mucaho
2
@BasileStarynkevitch Su punto es que en un mundo ideal, sí, la redundancia es mala, pero en la práctica, donde no puede prever todos los aspectos relevantes, ¿es mejor prevenir que curar?
mucaho
13

Mire el asunto de esta manera: solo debe escribir código que sea (a) correcto (de lo contrario, su programa es completamente incorrecto) y (b) necesario (de lo contrario, su código es demasiado grande, lo que significa más RAM necesaria, más ciclos gastados en cosas inútiles, más esfuerzo para entenderlo, más tiempo dedicado a mantenerlo, etc., etc.)

Ahora considere qué es lo que quiere hacer en un finalizador. O es necesario. En ese caso, no puede colocarlo en un finalizador, porque no sabe si se llamará. Eso no es lo suficientemente bueno. O no es necesario, ¡entonces no deberías escribirlo en primer lugar! De cualquier manera, ponerlo en el finalizador es la elección incorrecta.

(Tenga en cuenta que los ejemplos que nombra, como cerrar secuencias de archivos, parecen no ser realmente necesarios, pero lo son. Es solo que hasta que llegue al límite de los identificadores de archivos abiertos en su sistema, no notará que su código es incorrecto. Pero ese límite es una característica del sistema operativo y por lo tanto es aún más impredecible que la política de la JVM para finalizadores, por lo que realmente es importante que usted no pierde identificadores de archivo.)

Kilian Foth
fuente
Si solo se supone que debo escribir el código que es "necesario", ¿debería evitar todo el estilo en todas mis aplicaciones GUI? Seguramente nada de eso es necesario; las GUI funcionarán bien sin estilo, simplemente se verán horribles. Sobre el finalizador, puede ser necesario hacer algo, pero aún así está bien ponerlo en un finalizador, ya que se llamará al finalizador durante el objeto GC, si es que lo hace. Si necesita cerrar un recurso, específicamente cuando está listo para la recolección de basura, entonces un finalizador es óptimo. O el programa termina de liberar su recurso o se llama al finalizador.
Kröw
Si tengo un objeto que se puede cerrar, pesado, costoso de crear RAM y decido hacer referencia a él débilmente, para que pueda borrarse cuando no sea necesario, entonces podría usarlo finalize()para cerrarlo en caso de que un ciclo gc realmente necesite liberarse RAM. De lo contrario, mantendría el objeto en la RAM en lugar de tener que regenerarlo y cerrarlo cada vez que necesite usarlo. Claro, los recursos que abre no se liberarán hasta que el objeto sea GCed, siempre que sea posible, pero es posible que no necesite garantizar que mis recursos se liberen en un momento determinado.
Kröw
8

Una de las principales razones para no confiar en los finalizadores es que la mayoría de los recursos que uno podría estar tentado a limpiar en el finalizador son muy limitados. El recolector de basura solo se ejecuta de vez en cuando, ya que atravesar referencias para determinar si algo se puede liberar o no es costoso. Esto significa que podría pasar 'un tiempo' antes de que tus objetos se destruyan realmente. Si tiene muchos objetos que abren conexiones de bases de datos de corta duración, por ejemplo, dejar que los finalizadores limpien estas conexiones podría agotar su grupo de conexiones mientras espera que el recolector de basura finalmente se ejecute y libere las conexiones terminadas. Luego, debido a la espera, terminas con una gran cantidad de pedidos pendientes en cola, que agotan rápidamente el grupo de conexiones nuevamente. Eso'

Además, el uso de prueba con recursos hace que cerrar objetos 'cerrables' al finalizar sea fácil de hacer. Si no está familiarizado con esta construcción, le sugiero que la consulte: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Perros
fuente
8

Además de por qué dejar que el finalizador libere recursos es generalmente una mala idea, los objetos finalizables vienen con una sobrecarga de rendimiento.

De la teoría y práctica de Java: recolección y rendimiento de basura (Brian Goetz), los finalizadores no son tus amigos :

Los objetos con finalizadores (aquellos que tienen un método no trivial finalize ()) tienen una sobrecarga significativa en comparación con los objetos sin finalizadores, y deben usarse con moderación. Los objetos finalizables son más lentos de asignar y más lentos de recolectar. En el momento de la asignación, la JVM debe registrar cualquier objeto finalizable con el recolector de basura, y (al menos en la implementación de la JVM de HotSpot) los objetos finalizables deben seguir una ruta de asignación más lenta que la mayoría de los otros objetos. Del mismo modo, los objetos finalizables también son más lentos de recolectar. Se necesitan al menos dos ciclos de recolección de basura (en el mejor de los casos) antes de que se pueda reclamar un objeto finalizable, y el recolector de basura tiene que hacer un trabajo adicional para invocar al finalizador. El resultado es más tiempo dedicado a la asignación y recolección de objetos y más presión sobre el recolector de basura, porque la memoria utilizada por los objetos finalizables inalcanzables se conserva más tiempo. Combine eso con el hecho de que no se garantiza que los finalizadores se ejecuten en un período de tiempo predecible, o incluso en absoluto, y puede ver que hay relativamente pocas situaciones en las que la finalización es la herramienta adecuada para usar.

alip
fuente
Excelente punto Vea mi respuesta que proporciona un medio para evitar la sobrecarga de rendimiento de finalize().
Mike Nakis
7

Mi (menos) razón favorita para evitar Object.finalizees que los objetos pueden finalizar después de que lo esperas, sino que pueden finalizarse antes de que puedas esperarlo. El problema no es que un objeto que todavía está dentro del alcance se pueda finalizar antes de salir del alcance si Java decide que ya no es accesible.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

Vea esta pregunta para más detalles. Aún más divertido es que esta decisión solo se puede tomar después de que la optimización del punto caliente se active, haciendo que sea doloroso depurarla.

Michael Anderson
fuente
1
La cuestión es que HasFinalize.streamdebería ser un objeto finalizable por separado. Es decir, la finalización de HasFinalizeno debería finalizar o intentar limpiar stream. O si debería, entonces debería hacer streaminaccesible.
acelent
1
Es aún peor
Holger, el
4

Tengo que verificar constantemente si un objeto tiene una implementación de close (), y estoy seguro de que he perdido algunas llamadas en algunos puntos en el pasado.

En Eclipse, recibo una advertencia cada vez que olvido cerrar algo que implementa Closeable/ AutoCloseable. No estoy seguro de si eso es algo de Eclipse o si es parte del compilador oficial, pero podría considerar el uso de herramientas de análisis estático similares para ayudarlo allí. FindBugs, por ejemplo, probablemente pueda ayudarlo a verificar si ha olvidado cerrar un recurso.

Nebu Pookins
fuente
1
Buena idea mencionar AutoCloseable. Hace que sea muy fácil administrar recursos con la función de probar con recursos. Esto anula varios de los argumentos en la pregunta.
2

A tu primera pregunta:

Depende de la implementación de la VM y del GC determinar cuándo es el momento adecuado para desasignar un objeto, no el desarrollador. ¿Por qué es importante decidir cuándo Object.finalize()se llama?

Bueno, la JVM determinará cuándo es un buen punto reclamar el almacenamiento que se ha asignado a un objeto. Este no es necesariamente el momento en que debe realizarse la limpieza del recurso, que desea realizar finalize(). Esto se ilustra en la pregunta "finalize () llamada a un objeto muy accesible en Java 8" sobre SO. Allí, un close()método ha sido invocado por un finalize()método, mientras que todavía está pendiente un intento de leer del flujo por el mismo objeto. Por lo tanto, además de la conocida posibilidad que finalize()se llama demasiado tarde, existe la posibilidad de que se llame demasiado temprano.

La premisa de su segunda pregunta:

Normalmente, y corríjame si me equivoco, el único momento en Object.finalize()que no se llamaría es cuando la aplicación se terminara antes de que el GC tuviera la oportunidad de ejecutarse.

Simplemente está mal. No hay ningún requisito para que una JVM admita la finalización en absoluto. Bueno, no está completamente equivocado, ya que aún podría interpretarlo como "la aplicación se finalizó antes de que finalice la finalización", suponiendo que su aplicación alguna vez finalice.

Pero tenga en cuenta la pequeña diferencia entre "GC" de su declaración original y el término "finalización". La recolección de basura es distinta a la finalización. Una vez que la administración de memoria detecta que un objeto es inalcanzable, simplemente puede reclamar su espacio, si no tiene un finalize()método especial o la finalización simplemente no es compatible, o puede poner en cola el objeto para su finalización. Por lo tanto, la finalización de un ciclo de recolección de basura no implica que se ejecuten los finalizadores. Eso puede suceder más adelante, cuando se procesa la cola, o nunca.

Este punto es también la razón por la cual incluso en JVM con soporte de finalización, confiar en él para la limpieza de recursos es peligroso. La recolección de basura es parte de la administración de memoria y, por lo tanto, se desencadena por las necesidades de memoria. Es posible que la recolección de basura nunca se ejecute porque hay suficiente memoria durante todo el tiempo de ejecución (bueno, eso todavía encaja en la descripción de "la aplicación se terminó antes de que el GC tenga la oportunidad de ejecutarse", de alguna manera). También es posible que el GC se ejecute, pero luego, hay suficiente memoria reclamada, por lo que la cola del finalizador no se procesa.

En otras palabras, los recursos nativos administrados de esta manera siguen siendo ajenos a la administración de la memoria. Si bien está garantizado que OutOfMemoryErrorsolo se lanza después de suficientes intentos de liberar memoria, eso no se aplica a los recursos nativos y la finalización. Es posible que la apertura de un archivo falle debido a recursos insuficientes mientras la cola de finalización está llena de objetos que podrían liberar estos recursos, si el finalizador alguna vez se ejecutó ...

Holger
fuente