Útmutató a Java véglegesítéséhez

1. Áttekintés

Ebben az oktatóanyagban a Java nyelv egyik fő szempontjára - a véglegesíteni a gyökér által biztosított módszer Tárgy osztály.

Egyszerűen fogalmazva, ezt egy adott tárgy szemétszállítása előtt hívják meg.

2. A véglegesítők használata

A véglegesítés () metódust véglegesítőnek nevezzük.

A véglegesítőket akkor hívják meg, amikor a JVM rájön, hogy ezt a konkrét példányt szemétszedésnek kell lennie. Az ilyen véglegesítő bármilyen műveletet végezhet, beleértve az objektum életre keltését is.

A véglegesítő fő célja azonban az objektumok által használt erőforrások felszabadítása, mielőtt eltávolítanák őket a memóriából. A véglegesítő működhet a tisztítási műveletek elsődleges mechanizmusaként, vagy más hálózatok sikertelensége esetén biztonsági hálóként.

A véglegesítő működésének megértéséhez vessünk egy pillantást egy osztálynyilatkozatra:

nyilvános osztály Véglegesíthető {private BufferedReader olvasó; public Finalizable () {InputStream input = this.getClass () .getClassLoader () .getResourceAsStream ("file.txt"); this.reader = new BufferedReader (új InputStreamReader (input)); } public String readFirstLine () dobja az IOException {String firstLine = olvasó.readLine (); return firstLine; } // az osztály többi tagja}

Osztály Véglegesíthető mezője van olvasó, amely egy bezárható erőforrásra hivatkozik. Amikor egy objektum ebből az osztályból jön létre, akkor újat épít BufferedReader példány olvasása az osztályútvonal fájljából.

Ilyen példányt használnak a readFirstLine módszer az adott fájl első sorának kibontására. Vegye figyelembe, hogy az olvasó nincs bezárva a megadott kódban.

Megtehetjük a véglegesítő segítségével:

@Orride public void finalize () {try {reader.close (); System.out.println ("Zárt pufferelt olvasó a véglegesítőben"); } catch (IOException e) {// ...}}

Könnyen belátható, hogy a véglegesítőt ugyanúgy deklarálják, mint bármely normális példányos módszert.

A valóságban, az idő, amikor a szemétszedő felhívja a véglegesítőket, a JVM implementációjától és a rendszer feltételeitől függ, amelyek nem tartoznak ellenőrzésünk alá.

Ahhoz, hogy a szemétszállítás a helyszínen történjen, kihasználjuk a System.gc módszer. A valós rendszerekben soha nem szabad erre hivatkoznunk, különféle okokból:

  1. Ez költséges
  2. Nem váltja ki azonnal a szemétszállítást - ez csak egy tipp arra, hogy a JVM elindítsa a GC-t
  3. A JVM jobban tudja, mikor kell felhívni a GC-t

Ha kényszeríteni kell a GC-t, használhatjuk jconsole azért.

Az alábbiakban bemutatjuk a véglegesítő működését bemutató tesztesetet:

@Test public void, amikor a GC_thenFinalizerExecuted () dobja az IOException {String firstLine = new Finalizable (). ReadFirstLine (); assertEquals ("baeldung.com", firstLine); System.gc (); }

Az első állításban a Véglegesíthető objektum jön létre, akkor annak readFirstLine módszert nevezzük. Ez az objektum nincs hozzárendelve egyetlen változóhoz sem, ezért jogosult szemétgyűjtésre, amikor a System.gc metódust hívják meg.

A teszt állítása igazolja a bemeneti fájl tartalmát, és csak annak bizonyítására szolgál, hogy egyéni osztályunk a várakozásoknak megfelelően működik.

Amikor lefuttatjuk a megadott tesztet, a konzolra üzenetet nyomtatunk arról, hogy a pufferelt olvasó bezárult a véglegesítőben. Ez azt jelenti, hogy véglegesíteni metódust hívták meg, és megtisztította az erőforrást.

A véglegesítők mindeddig remek módnak tűnnek a megsemmisítés előtti műveletek számára. Ez azonban nem egészen igaz.

A következő szakaszban megtudjuk, miért kell kerülni a használatukat.

3. Kerülje a véglegesítőket

Az általuk nyújtott előnyök ellenére a véglegesítők sok hátránnyal járnak.

3.1. A véglegesítők hátrányai

Vessen egy pillantást több problémára, amelyekkel szembesülünk, amikor a véglegesítőket kritikus műveletek végrehajtására használjuk.

Az első észrevehető kérdés a gyorsaság hiánya. Nem tudhatjuk, mikor fut a véglegesítő, mivel a szemétszállítás bármikor megtörténhet.

Önmagában ez nem jelent problémát, mert a véglegesítő előbb-utóbb mégis végrehajtja. A rendszererőforrások azonban nem korlátlanok. Így elfogyhatnak az erőforrásaink, mielőtt megtisztítás történik, ami a rendszer összeomlásához vezethet.

A véglegesítők hatással vannak a program hordozhatóságára is. Mivel a szemétszedési algoritmus JVM-implementációtól függ, előfordulhat, hogy egy program nagyon jól fut az egyik rendszeren, miközben másként másként viselkedik.

A teljesítmény költsége egy másik jelentős kérdés, amely a véglegesítőkkel jár. Kimondottan, A JVM-nek sokkal több műveletet kell végrehajtania, amikor nem üres véglegesítőt tartalmazó objektumokat készít és rombol.

Az utolsó probléma, amiről beszélni fogunk, a kivétel kezelésének hiánya a véglegesítés során. Ha a véglegesítő kivételt vet, a véglegesítési folyamat leáll, és az objektum értesítés nélkül sérült állapotban marad.

3.2. A véglegesítők hatásainak bemutatása

Itt az ideje félretenni az elméletet és megnézni a véglegesítők hatásait a gyakorlatban.

Definiáljunk egy új osztályt egy nem üres véglegesítővel:

public class CrashedFinalizable {public static void main (String [] érvelés) dob ReflectiveOperationException {for (int i = 0;; i ++) {new CrashedFinalizable (); // egyéb kód}} @Orride protected void finalize () {System.out.print (""); }}

Figyelje meg a véglegesítés () módszer - csak kiír egy üres karakterláncot a konzolra. Ha ez a módszer teljesen üres lenne, a JVM úgy kezelné az objektumot, mintha nem lenne véglegesítője. Ezért biztosítanunk kell véglegesítés () megvalósítással, amely ebben az esetben szinte semmit sem tesz.

Benne fő- módszer, egy új CrashedFinalizable példány jön létre a mert hurok. Ez a példány nincs hozzárendelve egyetlen változóhoz sem, ezért jogosult a szemétszállításra.

Adjunk hozzá néhány állítást a -val jelölt sorhoz // egyéb kód megnézni, hogy hány objektum van a memóriában futás közben:

if ((i% 1_000_000) == 0) {Class finalizerClass = Class.forName ("java.lang.ref.Finalizer"); Field queueStaticField = finalizerClass.getDeclaredField ("sor"); 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 ("% d hivatkozás van a% n sorban", queueLength); }

A megadott utasítások elérik a belső JVM osztályok egyes mezőit, és minden millió iteráció után kinyomtatják az objektum hivatkozások számát.

Indítsuk el a programot a fő- módszer. Várhatjuk, hogy a végtelenségig működjön, de ez nem így van. Néhány perc múlva látnunk kell a rendszer összeomlását ehhez hasonló hibával:

... A sorban 21914844 hivatkozás található. A sorban 22858923 hivatkozás található. A sorban 24202629 hivatkozás található. Az üzenetsorban 24621725 hivatkozás található. A sorban 25410983 hivatkozás található. a sor kivétele a "main" szálban java.lang.OutOfMemoryError: A GC általános korlátja túllépve a java.lang.ref.Finalizer.register (Finalizer.java:91) címen a java.lang.Object. (Object.java:37) címen. com.baeldung.finalize.CrashedFinalizable. (CrashedFinalizable.java:6) at com.baeldung.finalize.CrashedFinalizable.main (CrashedFinalizable.java:9) Az 1. kilépési kóddal befejezett folyamat

Úgy tűnik, hogy a szemétszedő nem tette jól a dolgát - az objektumok száma folyamatosan nőtt, amíg a rendszer összeomlott.

Ha eltávolítanánk a véglegesítőt, a hivatkozások száma általában 0 lenne, és a program örökké futna.

3.3. Magyarázat

Annak megértéséhez, hogy a szemétgyűjtő miért nem dobta el az objektumokat, ahogy kell, meg kell vizsgálnunk, hogyan működik a JVM belsőleg.

Olyan objektum létrehozásakor, amelyet referensnek is neveznek, és rendelkezik véglegesítővel, a JVM létrehoz egy kísérő típusú objektumot java.lang.ref.Finalizer. Miután a referens készen áll a szemétszállításra, a JVM feldolgozásra készként megjelöli a referenciaobjektumot, és referenciasorba helyezi.

Ehhez a sorhoz a statikus mezőn keresztül férhetünk hozzá sor ban,-ben java.lang.ref.Finalizer osztály.

Közben egy speciális démon szálat hívtak Véglegesítő folyamatosan fut, és objektumokat keres a referencia sorban. Ha talál, eltávolítja a referenciaobjektumot a sorból, és felhívja a véglegesítőt a referensen.

A következő szemétgyűjtési ciklus során a referens eldobásra kerül - amikor már nem hivatkoznak rá referenciaobjektumról.

Ha egy szál folyamatosan nagy sebességgel állít elő tárgyakat, akkor mi történt a példánkban, az Véglegesítő szál nem tud lépést tartani. Végül a memória nem lesz képes tárolni az összes objektumot, és végül egy OutOfMemoryError.

Figyeljen meg egy olyan helyzetre, amikor az objektumok láncsebességgel készülnek, amint az ebben a szakaszban látható, a való életben nem gyakran fordul elő. Fontos szempontot mutat azonban - a véglegesítők nagyon drágák.

4. Nem véglegesítő példa

Fedezzünk fel egy megoldást, amely ugyanazt a funkcionalitást biztosítja, de anélkül, hogy használná véglegesítés () módszer. Vegye figyelembe, hogy az alábbi példa nem az egyetlen módja a véglegesítők cseréjének.

Ehelyett egy fontos szempont bemutatására használják: mindig vannak olyan lehetőségek, amelyek segítenek elkerülni a véglegesítőket.

Íme az új osztályunk nyilatkozata:

public class CloseableResource megvalósítja az AutoCloseable {private BufferedReader olvasót; public CloseableResource () {InputStream input = this.getClass () .getClassLoader () .getResourceAsStream ("file.txt"); olvasó = new BufferedReader (új InputStreamReader (input)); } public String readFirstLine () dobja az IOException {String firstLine = olvasó.readLine (); return firstLine; } @Orride public void close () {try {reader.close (); System.out.println ("Closed BufferedReader a close módszerben"); } catch (IOException e) {// kivétel kezelése}}}

Nem nehéz belátni, hogy az egyetlen különbség az új között CloseableResource osztály és az előzőnk Véglegesíthető osztály a Automatikusan zárható felületet a véglegesítő definíciója helyett.

Figyelje meg, hogy a Bezárás a metódusa CloseableResource majdnem megegyezik az osztály véglegesítőjének testével Véglegesíthető.

Az alábbiakban bemutatunk egy tesztelési módszert, amely beolvassa a bemeneti fájlt és felszabadítja az erőforrást a munka befejezése után:

@Test public void, amikor aTryWResourcesExits_thenResourceClosed () az IOException-t dobja {try (CloseableResource erőforrás = new CloseableResource ()) {String firstLine = resource.readFirstLine (); assertEquals ("baeldung.com", firstLine); }}

A fenti tesztben a CloseableResource példány jön létre a próbáld ki a try-with-resources utasítás utasítás blokkja, ezért az erőforrás automatikusan bezárul, amikor a try-with-resources blokk befejezi a végrehajtást.

A megadott vizsgálati módszer futtatásakor látni fogunk egy üzenetet, amelyet kinyomtatunk a Bezárás módszere CloseableResource osztály.

5. Következtetés

Ebben az oktatóanyagban a Java egyik alapkoncepciójára - a véglegesíteni módszer. Ez papíron hasznosnak tűnik, de futás közben csúnya mellékhatásai lehetnek. És ami még fontosabb, mindig létezik alternatív megoldás a véglegesítő használatára.

Az egyik kritikus pont, amit észre kell venni véglegesíteni a Java 9-től kezdődően elavult - és végül eltávolításra kerül.

Mint mindig, az oktatóanyag forráskódja megtalálható a GitHubon.