Vodič za finaliziranje metode u Javi

1. Pregled

U ovom uputstvu usredotočit ćemo se na ključni aspekt Java jezika - dovršiti metoda koju osigurava korijen Objekt razred.

Jednostavno rečeno, to se zove prije odvoza smeća za određeni objekt.

2. Korištenje finalizatora

The finalizirati () metoda naziva se finalizator.

Finalizatori se pozivaju kada JVM shvati da bi ovaj primjerak trebao biti sakupljeno smeće. Takav finalizator može izvoditi sve radnje, uključujući vraćanje predmeta u život.

Međutim, glavna svrha finalizatora je oslobađanje resursa koje objekti koriste prije nego što se uklone iz memorije. Završno sredstvo može raditi kao primarni mehanizam čišćenja ili kao zaštitna mreža kada druge metode zakažu.

Da bismo razumjeli kako finalizator radi, pogledajmo deklaraciju klase:

javna klasa Finalizable {private BufferedReader čitač; javno Finalizable () {InputStream input = this.getClass () .getClassLoader () .getResourceAsStream ("file.txt"); this.reader = novi BufferedReader (novi InputStreamReader (ulaz)); } javni String readFirstLine () baca IOException {String firstLine = reader.readLine (); povratak firstLine; } // ostali članovi predmeta}

Razred Konačno ima polje čitač, koji upućuje na bliski resurs. Kada se objekt stvori iz ove klase, on konstruira novi BufferedReader primjer čitanje iz datoteke u razrednoj stazi.

Takva se instanca koristi u readFirstLine metoda za izdvajanje prvog retka u datoj datoteci. Primijetite da čitač nije zatvoren u danom kodu.

To možemo učiniti pomoću finalizatora:

@Preuzmi javnu prazninu finalize () {try {reader.close (); System.out.println ("Closed BufferedReader u završnom programu"); } catch (IOException e) {// ...}}

Lako je vidjeti da je finalizator deklariran baš kao i svaka uobičajena metoda instance.

U stvarnosti, vrijeme u kojem sakupljač smeća poziva finalizatore ovisi o provedbi JVM-a i uvjetima sustava, koji su izvan naše kontrole.

Da bi se odvoz smeća dogodio na licu mjesta, mi ćemo iskoristiti System.gc metoda. U stvarnim sustavima nikada se ne bismo trebali izričito pozivati ​​na to iz više razloga:

  1. To je skupo
  2. To ne pokreće odvoz smeća odmah - to je samo nagovještaj da JVM pokrene GC
  3. JVM bolje zna kada treba nazvati GC

Ako trebamo prisiliti GC, možemo koristiti jconsole za to.

Slijedi testni slučaj koji pokazuje rad finalizatora:

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

U prvoj izjavi, a Konačno stvoren je objekt, a zatim njegov readFirstLine metoda se naziva. Ovaj objekt nije dodijeljen nijednoj varijabli, stoga ispunjava uvjete za odvoz smeća kada System.gc poziva se metoda.

Tvrdnja u testu provjerava sadržaj ulazne datoteke i koristi se samo da dokaže da naša prilagođena klasa radi kako se očekivalo.

Kada pokrenemo ponuđeni test, na konzoli će se ispisati poruka o zatvaranju puferiranog čitača u finalizatoru. To podrazumijeva dovršiti pozvana je metoda i očistila je resurs.

Do ovog trenutka finalizatori izgledaju kao izvrstan način za operacije pred-uništenja. Međutim, to nije sasvim točno.

U sljedećem ćemo odjeljku vidjeti zašto ih treba izbjegavati.

3. Izbjegavanje finalizatora

Unatoč prednostima koje donose, finalizatori imaju mnogo nedostataka.

3.1. Mane finalizatora

Pogledajmo nekoliko problema s kojima ćemo se suočiti prilikom upotrebe finalizatora za izvođenje kritičnih radnji.

Prvo uočljivo pitanje je nedostatak ažurnosti. Ne možemo znati kada se finalizator pokreće, jer se odvoz smeća može dogoditi bilo kada.

To samo po sebi nije problem jer se finalizator ipak izvršava, prije ili kasnije. Međutim, resursi sustava nisu neograničeni. Stoga nam može ponestati resursa prije nego što se dogodi čišćenje, što može rezultirati padom sustava.

Finalizatori također utječu na prenosivost programa. Budući da algoritam prikupljanja smeća ovisi o implementaciji JVM-a, program se može vrlo dobro izvoditi na jednom sustavu, dok se na drugom ponaša drugačije.

Trošak izvedbe je još jedno značajno pitanje koje dolazi s finalizatorima. Posebno, JVM mora izvesti mnogo više operacija prilikom konstrukcije i uništavanja objekata koji sadrže neprazan finalizator.

Posljednji problem o kojem ćemo razgovarati je nedostatak postupanja s iznimkama tijekom finalizacije. Ako finalizator izbaci iznimku, postupak finalizacije se zaustavlja, ostavljajući objekt u oštećenom stanju bez ikakve obavijesti.

3.2. Demonstracija učinaka finalizatora

Vrijeme je da teoriju ostavimo po strani i vidimo efekte finalizatora u praksi.

Definirajmo novu klasu s praznim finalizatorom:

javna klasa CrashedFinalizable {public static void main (String [] args) baca ReflectiveOperationException {for (int i = 0;; i ++) {new CrashedFinalizable (); // drugi kod}} @Override protected void finalize () {System.out.print (""); }}

Primijetite finalizirati () metoda - samo ispisuje prazan niz na konzolu. Da je ova metoda potpuno prazna, JVM bi se prema objektu ponašao kao da nema finalizator. Stoga moramo pružiti finalizirati () s implementacijom, koja u ovom slučaju ne čini gotovo ništa.

Unutar glavni metoda, nova CrashedFinalizable instanca kreira se u svakoj iteraciji za petlja. Ova instanca nije dodijeljena nijednoj varijabli, stoga ispunjava uvjete za odvoz smeća.

Dodajmo nekoliko izjava u redak označen s // drugi kod da biste vidjeli koliko objekata postoji u memoriji za vrijeme izvođenja:

if ((i% 1_000_000) == 0) {Class finalizerClass = Class.forName ("java.lang.ref.Finalizer"); Polje queueStaticField = finalizerClass.getDeclaredField ("red čekanja"); queueStaticField.setAccessible (true); ReferenceQueue referenceQueue = (ReferenceQueue) queueStaticField.get (null); Polje queueLengthField = ReferenceQueue.class.getDeclaredField ("queueLength"); queueLengthField.setAccessible (true); long queueLength = (long) queueLengthField.get (referenceQueue); System.out.format ("U redu je% d referenci% n", queueLength); }

Dani izrazi pristupaju nekim poljima u internim JVM klasama i ispisuju broj referenci na objekt nakon svakih milijun iteracija.

Pokrenimo program izvršavanjem glavni metoda. Možemo očekivati ​​da će trajati neograničeno, ali to nije slučaj. Nakon nekoliko minuta trebali bismo vidjeti pad sustava sa pogreškom sličnom ovoj:

... U redu je 21914844 referenci. U redu je 22858923 referenci. U redu je 24202629 referenci. U redu je 24621725 referenci. U redu je 25410983 referenci. U redu je 26231621 referenci. U redu je 26975913 referenci. izuzetak reda u niti "main" java.lang.OutOfMemoryError: prekoračeno ograničenje GC-a na java.lang.ref.Finalizer.register (Finalizer.java:91) na java.lang.Object. (Object.java:37) na com.baeldung.finalize.CrashedFinalizable. (CrashedFinalizable.java:6) na com.baeldung.finalize.CrashedFinalizable.main (CrashedFinalizable.java:9) Proces je završen s izlaznim kodom 1

Izgleda da sakupljač smeća nije dobro obavio svoj posao - broj predmeta se povećavao sve dok se sustav nije srušio.

Ako bismo uklonili finalizator, broj referenci obično bi bio 0, a program bi nastavio raditi zauvijek.

3.3. Obrazloženje

Da bismo razumjeli zašto sakupljač smeća nije odbacio predmete kako bi trebao, moramo pogledati kako JVM interno radi.

Kada stvara objekt, koji se naziva i referent, koji ima finalizator, JVM stvara pripadajući referentni objekt tipa java.lang.ref.Finalizer. Nakon što je referent spreman za odvoz smeća, JVM označava referentni objekt kao spreman za obradu i stavlja ga u referentni red.

Ovom redu možemo pristupiti putem statičkog polja red u java.lang.ref.Finalizer razred.

U međuvremenu je nazvana posebna daemon nit Finalizator nastavlja raditi i traži predmete u referentnom redu. Kad ga pronađe, uklanja referentni objekt iz reda i poziva referentnu datoteku.

Tijekom sljedećeg ciklusa prikupljanja smeća, referent će biti odbačen - kada se više ne upućuje iz referentnog objekta.

Ako nit nastavlja proizvoditi predmete velikom brzinom, što se dogodilo u našem primjeru, Finalizator nit ne može pratiti. Na kraju, memorija neće moći pohraniti sve predmete i na kraju ćemo dobiti OutOfMemoryError.

Primijetite da se situacije u kojima se predmeti stvaraju brzinom osnovanja, kao što je prikazano u ovom odjeljku, ne događaju često u stvarnom životu. Međutim, pokazuje važnu poantu - finalizatori su vrlo skupi.

4. Primjer ne-finalizatora

Istražimo rješenje koje pruža istu funkcionalnost, ali bez upotrebe finalizirati () metoda. Primijetite da primjer u nastavku nije jedini način zamjene finalizatora.

Umjesto toga, koristi se za pokazivanje važne točke: uvijek postoje opcije koje nam pomažu izbjeći finalizatore.

Evo izjave naše nove klase:

javna klasa CloseableResource implementira AutoCloseable {private BufferedReader čitač; javni CloseableResource () {InputStream input = this.getClass () .getClassLoader () .getResourceAsStream ("file.txt"); čitač = novi BufferedReader (novi InputStreamReader (ulaz)); } javni String readFirstLine () baca IOException {String firstLine = reader.readLine (); povratak firstLine; } @Override public void close () {try {reader.close (); System.out.println ("Closed BufferedReader in the close method"); } catch (IOException e) {// obrada iznimke}}}

Nije teško uvidjeti da je jedina razlika između novog CloseableResource razred i naš prethodni Konačno klasa je provedba AutoCloseable sučelje umjesto definicije finalizatora.

Primijetite da je tijelo Zatvoriti metoda CloseableResource je gotovo isto kao i tijelo finalizatora u klasi Konačno.

Slijedi metoda ispitivanja koja čita ulaznu datoteku i oslobađa resurs nakon završetka posla:

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

U gore navedenom testu, a CloseableResource instanca kreira se u probati blok naredbe try-with-resources, stoga se taj resurs automatski zatvara kad block try-with-resources dovrši izvršenje.

Pokretanjem zadane metode ispitivanja vidjet ćemo poruku ispisanu s Zatvoriti metoda CloseableResource razred.

5. Zaključak

U ovom uputstvu usredotočili smo se na osnovni koncept u Javi - dovršiti metoda. Ovo na papiru izgleda korisno, ali tijekom izvođenja može imati ružne nuspojave. I, što je još važnije, uvijek postoji alternativno rješenje za upotrebu finalizatora.

Jedna kritična točka koju treba primijetiti je ta dovršiti je zastario počevši od Jave 9 - i na kraju će biti uklonjen.

Kao i uvijek, izvorni kod za ovu lekciju možete pronaći na GitHubu.