Una guía para el método finalize en Java

1. Información general

En este tutorial, nos centraremos en un aspecto central del lenguaje Java: el método de finalización proporcionado por la clase Object raíz .

En pocas palabras, esto se llama antes de la recolección de basura para un objeto en particular.

2. Uso de finalizadores

El método finalize () se llama finalizador.

Los finalizadores se invocan cuando JVM descubre que esta instancia en particular debe ser recolectada como basura. Dicho finalizador puede realizar cualquier operación, incluida la recuperación del objeto.

Sin embargo, el objetivo principal de un finalizador es liberar los recursos utilizados por los objetos antes de que se eliminen de la memoria. Un finalizador puede funcionar como mecanismo principal para las operaciones de limpieza o como red de seguridad cuando fallan otros métodos.

Para entender cómo funciona un finalizador, echemos un vistazo a una declaración de clase:

public class Finalizable { private BufferedReader reader; public Finalizable() { InputStream input = this.getClass() .getClassLoader() .getResourceAsStream("file.txt"); this.reader = new BufferedReader(new InputStreamReader(input)); } public String readFirstLine() throws IOException { String firstLine = reader.readLine(); return firstLine; } // other class members }

La clase Finalizable tiene un lector de campo , que hace referencia a un recurso que se puede cerrar. Cuando se crea un objeto a partir de esta clase, construye una nueva instancia de BufferedReader leyendo un archivo en la ruta de clase .

Esta instancia se utiliza en el método readFirstLine para extraer la primera línea del archivo dado. Tenga en cuenta que el lector no está cerrado en el código dado.

Podemos hacer eso usando un finalizador:

@Override public void finalize() { try { reader.close(); System.out.println("Closed BufferedReader in the finalizer"); } catch (IOException e) { // ... } }

Es fácil ver que un finalizador se declara como cualquier método de instancia normal.

En realidad, el momento en el que el recolector de basura llama a los finalizadores depende de la implementación de la JVM y de las condiciones del sistema, que están fuera de nuestro control.

Para que la recolección de basura ocurra en el lugar, aprovecharemos el método System.gc . En los sistemas del mundo real, nunca deberíamos invocar eso explícitamente, por varias razones:

  1. Es costoso
  2. No activa la recolección de basura de inmediato, es solo una pista para que la JVM inicie GC
  3. JVM sabe mejor cuándo es necesario llamar a GC

Si necesitamos forzar GC, podemos usar jconsole para eso.

El siguiente es un caso de prueba que demuestra el funcionamiento de un finalizador:

@Test public void whenGC_thenFinalizerExecuted() throws IOException { String firstLine = new Finalizable().readFirstLine(); assertEquals("baeldung.com", firstLine); System.gc(); }

En la primera instrucción, se crea un objeto Finalizable , luego se llama a su método readFirstLine . Este objeto no está asignado a ninguna variable, por lo que es elegible para la recolección de basura cuando se invoca el método System.gc .

La afirmación en la prueba verifica el contenido del archivo de entrada y se usa solo para demostrar que nuestra clase personalizada funciona como se esperaba.

Cuando ejecutamos la prueba proporcionada, se imprimirá un mensaje en la consola sobre el cierre del lector almacenado en el finalizador. Esto implica que se llamó al método finalize y se limpió el recurso.

Hasta este punto, los finalizadores parecen una excelente manera de realizar operaciones previas a la destrucción. Sin embargo, eso no es del todo cierto.

En la siguiente sección, veremos por qué se debe evitar su uso.

3. Evitar finalistas

A pesar de los beneficios que aportan, los finalizadores tienen muchos inconvenientes.

3.1. Desventajas de los finalizadores

Echemos un vistazo a varios problemas que enfrentaremos al usar finalizadores para realizar acciones críticas.

El primer problema notable es la falta de prontitud. No podemos saber cuándo se ejecuta un finalizador ya que la recolección de basura puede ocurrir en cualquier momento.

Por sí solo, esto no es un problema porque el finalizador aún se ejecuta, tarde o temprano. Sin embargo, los recursos del sistema no son ilimitados. Por lo tanto, es posible que nos quedemos sin recursos antes de que se realice una limpieza, lo que puede provocar un bloqueo del sistema.

Los finalizadores también tienen un impacto en la portabilidad del programa. Dado que el algoritmo de recolección de basura depende de la implementación de JVM, un programa puede ejecutarse muy bien en un sistema mientras se comporta de manera diferente en otro.

El costo de rendimiento es otro problema importante que viene con los finalizadores. Específicamente, JVM debe realizar muchas más operaciones al construir y destruir objetos que contienen un finalizador no vacío .

El último problema del que hablaremos es la falta de manejo de excepciones durante la finalización. Si un finalizador lanza una excepción, el proceso de finalización se detiene, dejando el objeto en un estado dañado sin ninguna notificación.

3.2. Demostración de los efectos de los finalizadores

Es hora de dejar la teoría a un lado y ver los efectos de los finalizadores en la práctica.

Definamos una nueva clase con un finalizador no vacío:

public class CrashedFinalizable { public static void main(String[] args) throws ReflectiveOperationException { for (int i = 0; ; i++) { new CrashedFinalizable(); // other code } } @Override protected void finalize() { System.out.print(""); } }

Observe el método finalize () : simplemente imprime una cadena vacía en la consola. Si este método estuviera completamente vacío, la JVM trataría el objeto como si no tuviera un finalizador. Por lo tanto, necesitamos proporcionar a finalize () una implementación, que en este caso no hace casi nada.

Dentro del método principal , se crea una nueva instancia CrashedFinalizable en cada iteración del bucle for . Esta instancia no está asignada a ninguna variable, por lo que es elegible para la recolección de basura.

Agreguemos algunas declaraciones en la línea marcada con // otro código para ver cuántos objetos existen en la memoria en tiempo de ejecución:

if ((i % 1_000_000) == 0) { Class finalizerClass = Class.forName("java.lang.ref.Finalizer"); Field queueStaticField = finalizerClass.getDeclaredField("queue"); queueStaticField.setAccessible(true); ReferenceQueue referenceQueue = (ReferenceQueue) queueStaticField.get(null); Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength"); queueLengthField.setAccessible(true); long queueLength = (long) queueLengthField.get(referenceQueue); System.out.format("There are %d references in the queue%n", queueLength); }

The given statements access some fields in internal JVM classes and print out the number of object references after every million iterations.

Let's start the program by executing the main method. We may expect it to run indefinitely, but that's not the case. After a few minutes, we should see the system crash with an error similar to this:

... There are 21914844 references in the queue There are 22858923 references in the queue There are 24202629 references in the queue There are 24621725 references in the queue There are 25410983 references in the queue There are 26231621 references in the queue There are 26975913 references in the queue Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.ref.Finalizer.register(Finalizer.java:91) at java.lang.Object.(Object.java:37) at com.baeldung.finalize.CrashedFinalizable.(CrashedFinalizable.java:6) at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9) Process finished with exit code 1

Looks like the garbage collector didn't do its job well – the number of objects kept increasing until the system crashed.

If we removed the finalizer, the number of references would usually be 0 and the program would keep running forever.

3.3. Explanation

To understand why the garbage collector didn't discard objects as it should, we need to look at how the JVM works internally.

When creating an object, also called a referent, that has a finalizer, the JVM creates an accompanying reference object of type java.lang.ref.Finalizer. After the referent is ready for garbage collection, the JVM marks the reference object as ready for processing and puts it into a reference queue.

We can access this queue via the static field queue in the java.lang.ref.Finalizer class.

Meanwhile, a special daemon thread called Finalizer keeps running and looks for objects in the reference queue. When it finds one, it removes the reference object from the queue and calls the finalizer on the referent.

During the next garbage collection cycle, the referent will be discarded – when it's no longer referenced from a reference object.

If a thread keeps producing objects at a high speed, which is what happened in our example, the Finalizer thread cannot keep up. Eventually, the memory won't be able to store all the objects, and we end up with an OutOfMemoryError.

Notice a situation where objects are created at warp speed as shown in this section doesn't often happen in real life. However, it demonstrates an important point – finalizers are very expensive.

4. No-Finalizer Example

Let's explore a solution providing the same functionality but without the use of finalize() method. Notice that the example below isn't the only way to replace finalizers.

Instead, it's used to demonstrate an important point: there are always options that help us to avoid finalizers.

Here's the declaration of our new class:

public class CloseableResource implements AutoCloseable { private BufferedReader reader; public CloseableResource() { InputStream input = this.getClass() .getClassLoader() .getResourceAsStream("file.txt"); reader = new BufferedReader(new InputStreamReader(input)); } public String readFirstLine() throws IOException { String firstLine = reader.readLine(); return firstLine; } @Override public void close() { try { reader.close(); System.out.println("Closed BufferedReader in the close method"); } catch (IOException e) { // handle exception } } }

It's not hard to see that the only difference between the new CloseableResource class and our previous Finalizable class is the implementation of the AutoCloseable interface instead of a finalizer definition.

Notice that the body of the close method of CloseableResource is almost the same as the body of the finalizer in class Finalizable.

The following is a test method, which reads an input file and releases the resource after finishing its job:

@Test public void whenTryWResourcesExits_thenResourceClosed() throws IOException { try (CloseableResource resource = new CloseableResource()) { String firstLine = resource.readFirstLine(); assertEquals("baeldung.com", firstLine); } }

In the above test, a CloseableResource instance is created in the try block of a try-with-resources statement, hence that resource is automatically closed when the try-with-resources block completes execution.

Running the given test method, we'll see a message printed out from the close method of the CloseableResource class.

5. Conclusion

En este tutorial, nos enfocamos en un concepto central en Java: el método finalize . Esto parece útil en papel, pero puede tener efectos secundarios desagradables en tiempo de ejecución. Y, lo que es más importante, siempre existe una solución alternativa al uso de un finalizador.

Un punto crítico a tener en cuenta es que finalizar se ha desaprobado a partir de Java 9 y, finalmente, se eliminará.

Como siempre, el código fuente de este tutorial se puede encontrar en GitHub.