Un guide de la méthode finalize en Java

1. Vue d'ensemble

Dans ce didacticiel, nous allons nous concentrer sur un aspect fondamental du langage Java - la méthode finalize fournie par la classe Object racine .

En termes simples, cela est appelé avant le garbage collection pour un objet particulier.

2. Utilisation des finaliseurs

La méthode finalize () est appelée le finaliseur.

Les finaliseurs sont appelés lorsque JVM découvre que cette instance particulière doit être récupérée. Un tel finaliseur peut effectuer toutes les opérations, y compris ramener l'objet à la vie.

Cependant, le but principal d'un finaliseur est de libérer les ressources utilisées par les objets avant qu'ils ne soient supprimés de la mémoire. Un finaliseur peut fonctionner comme mécanisme principal pour les opérations de nettoyage, ou comme filet de sécurité lorsque d'autres méthodes échouent.

Pour comprendre le fonctionnement d'un finaliseur, jetons un coup d'œil à une déclaration de 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 a un lecteur de champ , qui fait référence à une ressource fermable. Lorsqu'un objet est créé à partir de cette classe, il construit une nouvelle instance de BufferedReader lisant un fichier dans le chemin de classe .

Une telle instance est utilisée dans la méthode readFirstLine pour extraire la première ligne du fichier donné. Notez que le lecteur n'est pas fermé dans le code donné.

Nous pouvons le faire en utilisant un finaliseur:

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

Il est facile de voir qu'un finaliseur est déclaré comme n'importe quelle méthode d'instance normale.

En réalité, le moment auquel le garbage collector appelle les finaliseurs dépend de l'implémentation de la JVM et des conditions du système, qui sont hors de notre contrôle.

Pour que le garbage collection se produise sur place, nous tirerons parti de la méthode System.gc . Dans les systèmes du monde réel, nous ne devrions jamais invoquer cela explicitement, pour un certain nombre de raisons:

  1. C'est cher
  2. Cela ne déclenche pas immédiatement le garbage collection - c'est juste un indice pour que la JVM démarre GC
  3. JVM sait mieux quand GC doit être appelé

Si nous devons forcer GC, nous pouvons utiliser jconsole pour cela.

Voici un cas de test illustrant le fonctionnement d'un finaliseur:

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

Dans la première instruction, un objet Finalizable est créé, puis sa méthode readFirstLine est appelée. Cet objet n'est affecté à aucune variable, il est donc éligible pour le garbage collection lorsque la méthode System.gc est appelée.

L'assertion dans le test vérifie le contenu du fichier d'entrée et est utilisée uniquement pour prouver que notre classe personnalisée fonctionne comme prévu.

Lorsque nous exécutons le test fourni, un message sera imprimé sur la console indiquant que le lecteur tamponné est fermé dans le finaliseur. Cela implique que la méthode finalize a été appelée et qu'elle a nettoyé la ressource.

Jusqu'à présent, les finaliseurs semblent être un excellent moyen pour les opérations de pré-destruction. Cependant, ce n'est pas tout à fait vrai.

Dans la section suivante, nous verrons pourquoi leur utilisation doit être évitée.

3. Éviter les finaliseurs

Malgré les avantages qu'ils apportent, les finaliseurs présentent de nombreux inconvénients.

3.1. Inconvénients des finaliseurs

Jetons un coup d'œil à plusieurs problèmes auxquels nous serons confrontés lors de l'utilisation des finaliseurs pour effectuer des actions critiques.

Le premier problème notable est le manque de rapidité. Nous ne pouvons pas savoir quand un finaliseur s'exécute car le garbage collection peut se produire à tout moment.

En soi, ce n'est pas un problème car le finaliseur s'exécute toujours, tôt ou tard. Cependant, les ressources système ne sont pas illimitées. Ainsi, nous pouvons manquer de ressources avant qu'un nettoyage ne se produise, ce qui peut entraîner une panne du système.

Les finaliseurs ont également un impact sur la portabilité du programme. Étant donné que l'algorithme de garbage collection dépend de l'implémentation JVM, un programme peut très bien fonctionner sur un système tout en se comportant différemment sur un autre.

Le coût des performances est un autre problème important associé aux finaliseurs. Plus précisément, JVM doit effectuer de nombreuses autres opérations lors de la construction et de la destruction d'objets contenant un finaliseur non vide .

Le dernier problème dont nous allons parler est le manque de gestion des exceptions lors de la finalisation. Si un finaliseur lève une exception, le processus de finalisation s'arrête, laissant l'objet dans un état corrompu sans aucune notification.

3.2. Démonstration des effets des finaliseurs

Il est temps de mettre la théorie de côté et de voir les effets des finaliseurs dans la pratique.

Définissons une nouvelle classe avec un finaliseur non vide:

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

Remarquez la méthode finalize () - elle imprime simplement une chaîne vide sur la console. Si cette méthode était complètement vide, la JVM traiterait l'objet comme s'il n'avait pas de finaliseur. Par conséquent, nous devons fournir à finalize () une implémentation, qui ne fait presque rien dans ce cas.

Dans la méthode main , une nouvelle instance CrashedFinalizable est créée à chaque itération de la boucle for . Cette instance n'est affectée à aucune variable, donc éligible pour le garbage collection.

Ajoutons quelques instructions à la ligne marquée // avec un autre code pour voir combien d'objets existent dans la mémoire au moment de l'exécution:

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

Dans ce didacticiel, nous nous sommes concentrés sur un concept de base en Java - la méthode finalize . Cela semble utile sur le papier mais peut avoir des effets secondaires horribles au moment de l'exécution. Et, plus important encore, il existe toujours une solution alternative à l'utilisation d'un finaliseur.

Un point critique à noter est que finaliser est obsolète à partir de Java 9 - et sera finalement supprimé.

Comme toujours, le code source de ce didacticiel se trouve à l'adresse over sur GitHub.