Un ghid pentru metoda de finalizare în Java

1. Prezentare generală

În acest tutorial, ne vom concentra pe un aspect de bază al limbajului Java - metoda de finalizare oferită de clasa obiect Object .

Pur și simplu, acest lucru se numește înainte de colectarea gunoiului pentru un anumit obiect.

2. Utilizarea Finalizatorilor

Metoda finalize () se numește finalizator.

Finalizatorii sunt invocați când JVM află că această instanță specială ar trebui să fie colectată la gunoi. Un astfel de finalizator poate efectua orice operațiune, inclusiv readucerea obiectului la viață.

Scopul principal al unui finalizator este, totuși, să elibereze resursele utilizate de obiecte înainte ca acestea să fie eliminate din memorie. Un finalizator poate funcționa ca mecanism principal pentru operațiunile de curățare sau ca o plasă de siguranță atunci când alte metode eșuează.

Pentru a înțelege cum funcționează un finalizator, să aruncăm o privire la o declarație de clasă:

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 }

Clasa Finalizable are un cititor de câmp , care face referire la o resursă închizabilă. Când un obiect este creat din această clasă, acesta construiește o nouă instanță BufferedReader care citește dintr-un fișier din classpath.

O astfel de instanță este utilizată în metoda readFirstLine pentru a extrage prima linie din fișierul dat. Observați că cititorul nu este închis în codul dat.

Putem face acest lucru folosind un finalizator:

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

Este ușor de văzut că un finalizator este declarat la fel ca orice metodă normală de instanță.

În realitate, momentul în care colectorul de gunoi apelează finalizatoare depinde de implementarea JVM și de condițiile sistemului, care sunt în afara controlului nostru.

Pentru a face colectarea gunoiului pe loc, vom profita de metoda System.gc . În sistemele din lumea reală, nu ar trebui să invocăm acest lucru în mod explicit, din mai multe motive:

  1. Este costisitor
  2. Nu declanșează imediat colectarea gunoiului - este doar un indiciu pentru JVM pentru a porni GC
  3. JVM știe mai bine când trebuie apelat GC

Dacă trebuie să forțăm GC, putem folosi jconsole pentru asta.

Următorul este un caz de testare care demonstrează funcționarea unui finalizator:

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

În prima declarație, se creează un obiect Finalizabil , apoi se numește metoda sa readFirstLine . Acest obiect nu este atribuit niciunei variabile, prin urmare este eligibil pentru colectarea gunoiului atunci când este invocată metoda System.gc .

Afirmația din test verifică conținutul fișierului de intrare și este utilizată doar pentru a dovedi că clasa noastră personalizată funcționează conform așteptărilor.

Când vom rula testul furnizat, un mesaj va fi tipărit pe consolă despre faptul că cititorul tampon este închis în finalizator. Aceasta implică metoda de finalizare a fost apelată și a curățat resursa.

Până în acest moment, finalizatorii arată ca o modalitate excelentă de operațiuni de pre-distrugere. Cu toate acestea, nu este chiar adevărat.

În secțiunea următoare, vom vedea de ce ar trebui evitată utilizarea acestora.

3. Evitarea finalizatorilor

În ciuda beneficiilor pe care le aduc, finalizatorii vin cu multe dezavantaje.

3.1. Dezavantaje ale Finalizatorilor

Să aruncăm o privire asupra mai multor probleme cu care ne vom confrunta atunci când vom folosi finalizatori pentru a efectua acțiuni critice.

Prima problemă vizibilă este lipsa promptitudinii. Nu putem ști când rulează un finalizator, deoarece colectarea gunoiului poate avea loc oricând.

În sine, aceasta nu este o problemă, deoarece finalizatorul încă se execută, mai devreme sau mai târziu. Cu toate acestea, resursele sistemului nu sunt nelimitate. Astfel, s-ar putea să ne epuizăm resursele înainte ca o curățare să aibă loc, ceea ce poate duce la o blocare a sistemului.

Finalizatorii au, de asemenea, un impact asupra portabilității programului. Deoarece algoritmul de colectare a gunoiului depinde de implementarea JVM, un program poate rula foarte bine pe un sistem în timp ce se comportă diferit pe altul.

Costul de performanță este o altă problemă semnificativă care vine cu finalizatorii. Mai exact, JVM trebuie să efectueze mai multe operații atunci când construiește și distruge obiecte care conțin un finalizator care nu este gol .

Ultima problemă despre care vom vorbi este lipsa gestionării excepțiilor în timpul finalizării. Dacă un finalizator lansează o excepție, procesul de finalizare se oprește, lăsând obiectul într-o stare deteriorată fără nicio notificare.

3.2. Demonstrarea efectelor finalizatorilor

Este timpul să lăsăm teoria deoparte și să vedem efectele finalizatorilor în practică.

Să definim o nouă clasă cu un finalizator care nu este gol:

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

Observați metoda finalize () - imprimă doar un șir gol pe consolă. Dacă această metodă ar fi complet goală, JVM ar trata obiectul ca și cum nu ar avea un finalizator. Prin urmare, trebuie să oferim finalize () cu o implementare, care nu face aproape nimic în acest caz.

În interiorul metodei principale , o nouă instanță CrashedFinalizable este creată în fiecare iterație a buclei for . Această instanță nu este atribuită niciunei variabile, deci eligibilă pentru colectarea gunoiului.

Să adăugăm câteva afirmații la linia marcată cu // alt cod pentru a vedea câte obiecte există în memorie în timpul rulării:

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

În acest tutorial, ne-am concentrat pe un concept de bază în Java - metoda de finalizare . Acest lucru pare util pe hârtie, dar poate avea efecte secundare urâte în timpul rulării. Și, mai important, există întotdeauna o soluție alternativă la utilizarea unui finalizator.

Un punct critic de observat este că finalizarea a fost depreciată începând cu Java 9 - și va fi în cele din urmă eliminată.

Ca întotdeauna, codul sursă pentru acest tutorial poate fi găsit pe GitHub.