Una guida al metodo finalize in Java

1. Panoramica

In questo tutorial, ci concentreremo su un aspetto fondamentale del linguaggio Java: il metodo finalize fornito dalla classe Object radice .

In poche parole, questo viene chiamato prima della garbage collection per un particolare oggetto.

2. Utilizzo dei finalizzatori

Il metodo finalize () è chiamato finalizer.

I finalizzatori vengono richiamati quando JVM scopre che questa particolare istanza deve essere raccolta in modo indesiderato. Tale finalizzatore può eseguire qualsiasi operazione, incluso riportare in vita l'oggetto.

Lo scopo principale di un finalizzatore è, tuttavia, rilasciare le risorse utilizzate dagli oggetti prima che vengano rimossi dalla memoria. Un finalizzatore può funzionare come meccanismo principale per le operazioni di pulizia o come rete di sicurezza quando altri metodi falliscono.

Per capire come funziona un finalizzatore, diamo un'occhiata a una dichiarazione di classe:

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 classe Finalizable ha un lettore di campo , che fa riferimento a una risorsa chiudibile. Quando un oggetto viene creato da questa classe, costruisce una nuova istanza BufferedReader che legge da un file nel classpath.

Tale istanza viene utilizzata nel metodo readFirstLine per estrarre la prima riga nel file specificato. Notare che il lettore non è chiuso nel codice dato.

Possiamo farlo utilizzando un finalizzatore:

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

È facile vedere che un finalizzatore viene dichiarato come un normale metodo di istanza.

In realtà, il momento in cui il garbage collector chiama i finalizzatori dipende dall'implementazione della JVM e dalle condizioni del sistema, che sono fuori dal nostro controllo.

Per fare in modo che la raccolta dei rifiuti avvenga sul posto, utilizzeremo il metodo System.gc . Nei sistemi del mondo reale, non dovremmo mai invocarlo esplicitamente, per una serie di motivi:

  1. È costoso
  2. Non attiva immediatamente la garbage collection: è solo un suggerimento per JVM per avviare GC
  3. JVM sa meglio quando è necessario chiamare GC

Se dobbiamo forzare GC, possiamo usare jconsole per questo.

Quello che segue è un test case che dimostra il funzionamento di un finalizzatore:

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

Nella prima istruzione viene creato un oggetto Finalizable , quindi viene chiamato il suo metodo readFirstLine . Questo oggetto non è assegnato a nessuna variabile, quindi è idoneo per la garbage collection quando viene richiamato il metodo System.gc .

L'asserzione nel test verifica il contenuto del file di input e viene utilizzata solo per dimostrare che la nostra classe personalizzata funziona come previsto.

Quando eseguiamo il test fornito, verrà stampato un messaggio sulla console riguardo alla chiusura del lettore bufferizzato nel finalizzatore. Ciò implica che il metodo finalize è stato chiamato e ha ripulito la risorsa.

Fino a questo punto, i finalizzatori sembrano un ottimo modo per le operazioni di pre-distruzione. Tuttavia, non è del tutto vero.

Nella prossima sezione vedremo perché è opportuno evitare di utilizzarli.

3. Evitare i finalizzatori

Nonostante i vantaggi che portano, i finalizzatori presentano molti inconvenienti.

3.1. Svantaggi dei finalizzatori

Diamo un'occhiata a diversi problemi che dovremo affrontare quando utilizzeremo i finalizzatori per eseguire azioni critiche.

Il primo problema evidente è la mancanza di tempestività. Non possiamo sapere quando viene eseguito un finalizzatore poiché la raccolta dei rifiuti può verificarsi in qualsiasi momento.

Di per sé, questo non è un problema perché il finalizzatore viene comunque eseguito, prima o poi. Tuttavia, le risorse di sistema non sono illimitate. Pertanto, potremmo esaurire le risorse prima che avvenga una pulizia, che potrebbe causare un arresto anomalo del sistema.

Finalizers also have an impact on the program's portability. Since the garbage collection algorithm is JVM implementation-dependent, a program may run very well on one system while behaving differently on another.

The performance cost is another significant issue that comes with finalizers. Specifically, JVM must perform many more operations when constructing and destroying objects containing a non-empty finalizer.

The last problem we'll be talking about is the lack of exception handling during finalization. If a finalizer throws an exception, the finalization process stops, leaving the object in a corrupted state without any notification.

3.2. Demonstration of Finalizers' Effects

It's time to put the theory aside and see the effects of finalizers in practice.

Let's define a new class with a non-empty finalizer:

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(""); } }

Notice the finalize() method – it just prints an empty string to the console. If this method were completely empty, the JVM would treat the object as if it didn't have a finalizer. Therefore, we need to provide finalize() with an implementation, which does almost nothing in this case.

Inside the main method, a new CrashedFinalizable instance is created in each iteration of the for loop. This instance isn't assigned to any variable, hence eligible for garbage collection.

Let's add a few statements at the line marked with // other code to see how many objects exist in the memory at runtime:

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

In questo tutorial, ci siamo concentrati su un concetto fondamentale in Java: il metodo finalize . Questo sembra utile sulla carta, ma può avere brutti effetti collaterali in fase di esecuzione. E, cosa più importante, c'è sempre una soluzione alternativa all'utilizzo di un finalizzatore.

Un punto critico da notare è che finalize è stato deprecato a partire da Java 9 e alla fine verrà rimosso.

Come sempre, il codice sorgente di questo tutorial può essere trovato su GitHub.