En guide til færdiggørelsesmetoden i Java

1. Oversigt

I denne vejledning fokuserer vi på et kerneaspekt af Java-sproget - færdiggør metode leveret af roden Objekt klasse.

Kort sagt kaldes dette før skraldopsamlingen for et bestemt objekt.

2. Brug af Finalizers

Det færdiggør () metode kaldes finalizer.

Finalister påberåbes, når JVM finder ud af, at denne særlige forekomst skal indsamles. En sådan færdigbehandler kan udføre enhver handling, herunder bringe genstanden til live igen.

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

For at forstå hvordan en finalizer fungerer, lad os se på en klassedeklaration:

offentlig klasse, der kan færdiggøres {privat BufferedReader-læser; offentlig færdiggørbar () {InputStream input = this.getClass () .getClassLoader () .getResourceAsStream ("file.txt"); this.reader = ny BufferedReader (ny InputStreamReader (input)); } offentlig String readFirstLine () kaster IOException {String firstLine = reader.readLine (); returner firstLine; } // andre klassemedlemmer}

Klassen Kan færdiggøres har et felt læser, der refererer til en ressource, der kan lukkes. Når et objekt oprettes fra denne klasse, konstruerer det et nyt BufferedReader eksempel læsning fra en fil i klassestien.

En sådan instans bruges i readFirstLine metode til at udtrække 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 hjælp af en finalizer:

@ Overstyr offentlig tomrum færdiggør () {prøv {reader.close (); System.out.println ("Closed BufferedReader in the finalizer"); } fange (IOException e) {// ...}}

Det er let at se, at en finalizer erklæres ligesom enhver normal instansmetode.

I virkeligheden, det tidspunkt, hvor affaldssamleren kalder færdigbehandlere, afhænger af JVM's implementering og systemets betingelser, som er uden for vores kontrol.

For at få affaldsindsamling til at ske på stedet drager vi fordel af System.gc metode. I virkelige systemer bør vi aldrig påberåbe os det eksplicit af en række årsager:

  1. Det er dyrt
  2. Det udløser ikke affaldssamlingen med det samme - det er bare et tip til JVM at starte GC
  3. JVM ved bedre, hvornår GC skal kaldes

Hvis vi har brug for at tvinge GC, kan vi bruge jconsole for det.

Følgende er en testtilfælde, der viser, hvordan en finalizer fungerer:

@Test offentlig ugyldig nårGC_thenFinalizerExecuted () kaster IOException {String firstLine = ny Finalizable (). ReadFirstLine (); assertEquals ("baeldung.com", firstLine); System.gc (); }

I den første erklæring, a Kan færdiggøres objekt oprettes, så er dens readFirstLine metode kaldes. Dette objekt er ikke tildelt nogen variabel, hvorfor det er kvalificeret til affaldsindsamling, når System.gc metode påberåbes.

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

Når vi kører den medfølgende test, udskrives en meddelelse på konsollen om, at den buffrede læser lukkes i finalisatoren. Dette indebærer, at færdiggør metode blev kaldt, og den har ryddet op i ressourcen.

Indtil dette tidspunkt ser finalisatorer ud som en fantastisk måde til pre-ødelæggende operationer. Det er dog ikke helt sandt.

I det næste afsnit ser vi, hvorfor brug af dem skal undgås.

3. Undgå færdiggørere

På trods af de fordele, de medfører, kommer finaliseringsprogrammet med mange ulemper.

3.1. Ulemper ved færdiggørere

Lad os se på flere problemer, vi står over for, når vi bruger finalizers til at udføre kritiske handlinger.

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

I sig selv er dette ikke et problem, fordi finalisatoren stadig udføres før eller senere. Systemressourcer er dog ikke ubegrænsede. Således kan vi løbe tør for ressourcer, inden der sker en oprydning, hvilket kan resultere i et systemnedbrud.

Finaliserere har også indflydelse på programmets bærbarhed. Da skraldopsamlingsalgoritmen er JVM-implementeringsafhængig, kan et program køre meget godt på et system, mens det opfører sig anderledes på et andet.

Ydeevneomkostningerne er et andet vigtigt problem, der følger med færdiggørere. Specifikt JVM skal udføre mange flere operationer, når de konstruerer og ødelægger objekter, der indeholder en ikke-tom finalizer.

Det sidste problem, vi vil tale om, er manglen på håndtering af undtagelser under færdiggørelsen. Hvis en finalizer kaster en undtagelse, stopper færdiggørelsesprocessen og efterlader objektet i en beskadiget tilstand uden nogen underretning.

3.2. Demonstration af Finalizers 'effekter

Det er tid til 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) kaster ReflectiveOperationException {for (int i = 0;; i ++) {new CrashedFinalizable (); // anden kode}} @ Override beskyttet ugyldighed finalize () {System.out.print (""); }}

Læg mærke til færdiggør () 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 finalizer. Derfor er vi nødt til at levere færdiggør () med en implementering, som næsten ikke gør noget i dette tilfælde.

Inde i vigtigste metode, en ny CrashedFinaliserbar forekomst oprettes i hver iteration af til løkke. Denne forekomst er ikke tildelt nogen variabel, 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:

hvis ((i% 1_000_000) == 0) {Class finalizerClass = Class.forName ("java.lang.ref.Finalizer"); Felt queueStaticField = finalizerClass.getDeclaredField ("kø"); queueStaticField.setAccessible (true); ReferenceQueue referenceQueue = (ReferenceQueue) queueStaticField.get (null); Field queueLengthField = ReferenceQueue.class.getDeclaredField ("queLength"); queueLengthField.setAccessible (true); lang køLængde = (lang) køLængdeFelt.get (referenceKø); System.out.format ("Der er% d referencer i køen% n", køLængde); }

De givne udsagn har 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 vigtigste metode. Vi kan forvente, at det kører på ubestemt tid, men det er ikke tilfældet. Efter et par minutter skal vi se systemet gå ned med en lignende fejl:

... Der er 21914844 referencer i køen Der er 22858923 referencer i køen Der er 24202629 referencer i køen Der er 24621725 referencer i køen Der er 25410983 referencer i køen Der er 26231621 referencer i køen Der er 26975913 referencer i køen køen Undtagelse i tråden "main" java.lang.OutOfMemoryError: GC-overheadgrænse overskredet ved java.lang.ref.Finalizer.register (Finalizer.java:91) ved java.lang.Object. (Object.java:37) kl. com.baeldung.finalize.CrashedFinalizable. (CrashedFinalizable.java:6) på com.baeldung.finalize.CrashedFinalizable.main (CrashedFinalizable.java:9) Process færdig med udgangskode 1

Det ser ud til, at affaldssamleren ikke gjorde sit job godt - antallet af genstande steg, indtil systemet styrtede ned.

Hvis vi fjernede finalisatoren, ville antallet af referencer normalt være 0, og programmet ville fortsætte med at køre for evigt.

3.3. Forklaring

For at forstå hvorfor affaldssamleren ikke kasserer genstande, som det skal, skal vi se på, hvordan JVM fungerer internt.

Når du opretter et objekt, også kaldet en referent, der har en finalizer, 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 placerer det i en referencekø.

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

I mellemtiden kaldes en speciel dæmontråd Finalizer fortsætter med at køre og ser efter objekter i referencekøen. Når den finder en, fjerner den referenceobjektet fra køen og kalder finalisatoren på referenten.

I løbet af den næste skraldopsamlingscyklus kasseres referenten - når den ikke længere refereres til fra et referenceobjekt.

Hvis en tråd fortsætter med at producere genstande i høj hastighed, er det, hvad der skete i vores eksempel, Finalizer tråden kan ikke følge med. Til sidst kan hukommelsen ikke gemme alle objekterne, og vi ender med en OutOfMemoryError.

Bemærk en situation, hvor genstande oprettes med kædehastighed som vist i dette afsnit, sker ikke ofte i det virkelige liv. Det viser dog et vigtigt punkt - slutbehandlere er meget dyre.

4. Ingen-finaliseringseksempel

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

I stedet bruges det til at demonstrere et vigtigt punkt: der er altid muligheder, der hjælper os med at undgå færdiggørere.

Her er erklæringen fra vores nye klasse:

offentlig klasse CloseableResource implementerer AutoCloseable {privat BufferedReader-læser; public CloseableResource () {InputStream input = this.getClass () .getClassLoader () .getResourceAsStream ("file.txt"); læser = ny BufferedReader (ny InputStreamReader (input)); } offentlig String readFirstLine () kaster IOException {String firstLine = reader.readLine (); returner firstLine; } @ Overstyr offentlig tomrum tæt () {prøv {reader.close (); System.out.println ("Closed BufferedReader in the close method"); } fange (IOException e) {// håndtag undtagelse}}}

Det er ikke svært at se, at den eneste forskel mellem det nye Lukbar ressource klasse og vores tidligere Kan færdiggøres klasse er implementeringen af Kan lukkes automatisk interface i stedet for en finaliseringsdefinition.

Bemærk, at kroppen af tæt metode til Lukbar ressource er næsten det samme som finaliseringsdelen i klassen Kan færdiggøres.

Følgende er en testmetode, der læser en inputfil og frigiver ressourcen efter afslutning af sit job:

@Test offentlig ugyldig når TryWResourcesExits_thenResourceClosed () kaster IOException {try (CloseableResource resource = new CloseableResource ()) {String firstLine = resource.readFirstLine (); assertEquals ("baeldung.com", firstLine); }}

I ovenstående test a Lukbar ressource forekomst oprettes i prøve blok af en prøve-med-ressourcer erklæring, derfor lukkes denne ressource automatisk, når prøve-med-ressourcer blokken fuldfører udførelsen.

Når vi kører den givne testmetode, ser vi en meddelelse udskrevet fra tæt metode til Lukbar ressource klasse.

5. Konklusion

I denne vejledning fokuserede vi på et kernekoncept i Java - the færdiggør metode. Dette ser nyttigt ud på papir, men kan have grimme bivirkninger ved kørsel. Og vigtigere er der altid en alternativ løsning til at bruge en finalizer.

Et kritisk punkt at bemærke er, at færdiggør er udfaset startende med Java 9 - og vil til sidst blive fjernet.

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