Java >> Java tutorial >  >> Tag >> final

En guide til færdiggørelsesmetoden i Java

1. Oversigt

I denne øvelse vil vi fokusere på et kerneaspekt af Java-sproget – afslut metode leveret af roden Objekt klasse.

Kort sagt kaldes dette før affaldsindsamlingen for en bestemt genstand.

2. Brug af afsluttere

finalize() metode kaldes finalizer.

Finalizers bliver påkaldt, når JVM finder ud af, at netop denne instans skal indsamles affald. En sådan færdiggører kan udføre enhver handling, herunder at bringe objektet til live igen.

Hovedformålet med en færdiggører er dog at frigive ressourcer brugt af objekter, før de fjernes fra hukommelsen. En færdiggører kan fungere som den primære mekanisme til oprydningsoperationer eller som et sikkerhedsnet, når andre metoder fejler.

For at forstå, hvordan en færdiggører fungerer, lad os tage et kig på en klasseerklæring:

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
}

Klassen afslutbar har en felt-læser , som refererer til en lukbar ressource. Når et objekt oprettes fra denne klasse, konstruerer det en ny BufferedReader instans læser fra en fil i klassestien.

En sådan instans bruges i readFirstLine metode til at udpakke den første linje i den givne fil. Bemærk, at læseren ikke er lukket i den givne kode.

Vi kan gøre det ved at bruge en færdiggører:

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

Det er let at se, at en færdiggører erklæres ligesom enhver normal instansmetode.

I virkeligheden er tidspunktet, hvor skraldeopsamleren ringer til færdigbehandlere, afhængig af JVM's implementering og systemets betingelser, som er uden for vores kontrol.

For at få affaldsindsamling til at ske på stedet, vil vi drage fordel af System.gc metode. I systemer i den virkelige verden bør vi aldrig påberåbe os det eksplicit af en række grunde:

  1. Det er dyrt
  2. Det udløser ikke affaldsindsamlingen med det samme – det er kun et tip til JVM'en om at starte GC
  3. JVM ved bedre, hvornår GC skal kaldes

Hvis vi skal tvinge GC, kan vi bruge jconsole for det.

Det følgende er en testcase, der demonstrerer funktionen af ​​en færdiggører:

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

I den første sætning er en Finaliserbar objektet er oprettet, og derefter dets readFirstLine metode kaldes. Dette objekt er ikke tildelt nogen variabel, og derfor er det kvalificeret til affaldsindsamling, når System.gc metoden påberåbes.

Påstanden i testen verificerer indholdet af inputfilen og bruges kun til at bevise, at vores tilpassede klasse fungerer som forventet.

Når vi kører den medfølgende test, vil der blive udskrevet en meddelelse på konsollen om, at bufferlæseren lukkes i finalizeren. Dette indebærer afslut metoden blev kaldt, og den har ryddet op i ressourcen.

Indtil dette tidspunkt ser færdiggørelsesprogrammer ud som en fantastisk måde til præ-destroy-operationer. Det er dog ikke helt rigtigt.

I næste afsnit vil vi se, hvorfor det bør undgås at bruge dem.

3. Undgå finalizers

På trods af de fordele, de bringer med sig, har færdiggørerne mange ulemper.

3.1. Ulemper ved Finalizers

Lad os se på flere problemer, vi står over for, når vi bruger færdiggørelsesværktøjer til at udføre kritiske handlinger.

Det første bemærkelsesværdige problem er manglen på hurtighed. Vi kan ikke vide, hvornår en færdiggører kører, da skraldindsamling kan forekomme når som helst.

I sig selv er dette ikke et problem, fordi færdiggørelsen stadig kører, før eller siden. Systemressourcer er dog ikke ubegrænsede. Således kan vi løbe tør for ressourcer, før der sker en oprydning, hvilket kan resultere i et systemnedbrud.

Finalizers har også en indflydelse på programmets portabilitet. Da affaldsindsamlingsalgoritmen er JVM-implementeringsafhængig, kan et program køre meget godt på ét system, mens det opfører sig anderledes på et andet.

Ydelsesomkostningerne er et andet væsentligt problem, der følger med færdiggørerne. SpecifiktJVM skal udføre mange flere operationer, når de konstruerer og ødelægger objekter, der indeholder en ikke-tom færdiggører .

Det sidste problem, vi vil tale om, er manglen på undtagelseshåndtering under færdiggørelsen. Hvis en færdiggører afgiver en undtagelse, stopper færdiggørelsesprocessen, hvilket efterlader objektet i en beskadiget tilstand uden nogen meddelelse.

3.2. Demonstration af finalizers' effekter

Det er på tide at lægge teorien til side og se virkningerne af finalizers i praksis.

Lad os definere en ny klasse med en ikke-tom 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("");
    }
}

Bemærk finalize() metode – den udskriver bare en tom streng til konsollen. Hvis denne metode var helt tom, ville JVM behandle objektet, som om det ikke havde en færdiggører. Derfor skal vi levere finalize() med en implementering, som næsten intet gør i dette tilfælde.

Inde i main metode, en ny CrashedFinalizable instans oprettes i hver iteration af for sløjfe. Denne forekomst er ikke tildelt nogen variabel og er derfor kvalificeret til affaldsindsamling.

Lad os tilføje et par udsagn på linjen markeret med // anden kode for at se, hvor mange objekter der findes i hukommelsen ved kørsel:

if ((i % 1_000_000) == 0) {
    Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
    Field queueStaticField = finalizerClass.getDeclaredField("queue");
    queueStaticField.setAccessible(true);
    ReferenceQueue<Object> 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);
}

De givne udsagn får adgang til nogle felter i interne JVM-klasser og udskriver antallet af objektreferencer efter hver million iterationer.

Lad os starte programmet ved at udføre main metode. Vi kan forvente, at det kører på ubestemt tid, men det er ikke tilfældet. Efter et par minutter skulle vi se systemet gå ned med en fejl svarende til denne:

...
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.<init>(Object.java:37)
    at com.baeldung.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
    at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)

Process finished with exit code 1

Det ser ud til, at skraldesamleren ikke gjorde sit arbejde godt – antallet af genstande blev ved med at stige, indtil systemet styrtede ned.

Hvis vi fjernede færdiggørelsen, ville antallet af referencer normalt være 0, og programmet ville blive ved med at køre for evigt.

3.3. Forklaring

For at forstå, hvorfor skraldeopsamleren ikke kasserede genstande, som den skulle, er vi nødt til at se på, hvordan JVM'en fungerer internt.

Når du opretter et objekt, også kaldet en referent, der har en færdiggører, opretter JVM et ledsagende referenceobjekt af typen java.lang.ref.Finalizer . Når referenten er klar til affaldsindsamling, markerer JVM referenceobjektet som klar til behandling og sætter det i en referencekø.

Vi kan få adgang til denne kø via det statiske felt i java.lang.ref.Finalizer klasse.

I mellemtiden, en speciel dæmontråd kaldet Finalizer bliver ved med at køre og leder efter objekter i referencekøen. Når den finder en, fjerner den referenceobjektet fra køen og kalder finalizeren på referenten.

Under den næste affaldsindsamlingscyklus vil referencen blive kasseret – når den ikke længere refereres fra et referenceobjekt.

Hvis en tråd bliver ved med at producere objekter med høj hastighed, hvilket er hvad der skete i vores eksempel, Finalizer tråden kan ikke følge med. Til sidst vil hukommelsen ikke være i stand til at gemme alle objekterne, og vi ender med en OutOfMemoryError .

Læg mærke til, at en situation, hvor objekter skabes med warp-hastighed, som vist i dette afsnit, ikke sker ofte i det virkelige liv. Det viser dog en vigtig pointe – finalizers er meget dyre.

4. No-Finalizer Eksempel

Lad os undersøge en løsning, der giver den samme funktionalitet, men uden brug af finalize() metode. Bemærk, at eksemplet nedenfor ikke er den eneste måde at erstatte finalizers på.

I stedet bruges det til at demonstrere en vigtig pointe:Der er altid muligheder, der hjælper os med at undgå finalister.

Her er erklæringen fra vores nye klasse:

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
        }
    }
}

Det er ikke svært at se, at den eneste forskel mellem den nye CloseableResource klasse og vores tidligere Finalizable klasse er implementeringen af ​​AutoCloseable grænseflade i stedet for en færdiggørelsesdefinition.

Bemærk, at brødteksten på lukket metoden CloseableResource er næsten den samme som teksten i finalisten i klassen Finalizable .

Følgende er en testmetode, som læser en inputfil og frigiver ressourcen efter at have afsluttet sit job:

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

I ovenstående test er en CloseableResource instans oprettes i try blok af en try-with-resources-sætning, derfor lukkes denne ressource automatisk, når try-with-ressources-blokken fuldfører eksekveringen.

Når vi kører den givne testmetode, vil vi se en meddelelse udskrevet fra luk metoden for CloseableResource klasse.

5 . Konklusion

I dette selvstudie fokuserede vi på et kernekoncept i Java – afslut metode. Dette ser nyttigt ud på papiret, men kan have grimme bivirkninger under kørsel. Og endnu vigtigere, der er altid en alternativ løsning til at bruge en færdiggører.

Et kritisk punkt at bemærke er, at afslutte er blevet forældet fra og med Java 9 – og vil til sidst blive fjernet.

Som altid kan kildekoden til denne tutorial findes på GitHub.


Java tag