Programmering

Objektafslutning og oprydning

For tre måneder siden begyndte jeg en mini-serie af artikler om design af objekter med en diskussion af designprincipper, der fokuserede på korrekt initialisering i begyndelsen af ​​et objekts liv. Heri Designteknikker artikel vil jeg fokusere på designprincipperne, der hjælper dig med at sikre korrekt oprydning i slutningen af ​​et objekts levetid.

Hvorfor rydde op?

Hvert objekt i et Java-program bruger databehandlingsressourcer, der er endelige. Naturligvis bruger alle objekter noget hukommelse til at gemme deres billeder på bunken. (Dette gælder selv for objekter, der erklærer ingen instansvariabler. Hvert objektbillede skal indeholde en slags markør til klassedata og kan også omfatte andre implementeringsafhængige oplysninger.) Men objekter kan også bruge andre endelige ressourcer udover hukommelsen. For eksempel kan nogle objekter bruge ressourcer såsom filhåndtag, grafiske sammenhænge, ​​sockets osv. Når du designer et objekt, skal du sørge for, at det til sidst frigiver de begrænsede ressourcer, det bruger, så systemet ikke løber tør for disse ressourcer.

Fordi Java er et sprog, der er indsamlet skrald, er det let at frigive hukommelsen, der er knyttet til et objekt. Alt du skal gøre er at slippe alle referencer til objektet. Fordi du ikke behøver at bekymre dig om eksplicit at frigøre et objekt, som du skal på sprog som C eller C ++, behøver du ikke bekymre dig om at ødelægge hukommelsen ved et uheld at frigøre det samme objekt to gange. Du skal dog sørge for, at du rent faktisk frigiver alle referencer til objektet. Hvis du ikke gør det, kan du ende med en hukommelseslækage, ligesom hukommelseslækage du får i et C ++ - program, når du glemmer at eksplicit frigøre objekter. Ikke desto mindre behøver du ikke bekymre dig om eksplicit at "frigøre" den hukommelse, så længe du frigiver alle referencer til et objekt.

På samme måde behøver du ikke bekymre dig om eksplicit at frigøre nogen bestanddele, der henvises til af instansvariablerne for et objekt, du ikke længere har brug for. Hvis du frigiver alle referencer til det unødvendige objekt, annulleres i virkeligheden enhver bestanddel, der er indeholdt i objektets instansvariabler. Hvis de nu ugyldige referencer var de eneste tilbageværende referencer til disse bestanddele, vil de bestanddele også være tilgængelige til affaldsindsamling. Et stykke kage, ikke?

Reglerne for affaldsindsamling

Selvom affaldsindsamling faktisk gør hukommelsesadministration i Java meget nemmere end det er i C eller C ++, er du ikke i stand til helt at glemme hukommelsen, når du programmerer i Java. For at vide, hvornår du muligvis har brug for at tænke på hukommelsesadministration i Java, skal du vide lidt om, hvordan affaldssamling behandles i Java-specifikationerne.

Affaldssamling er ikke påkrævet

Den første ting at vide er, at uanset hvor flittigt du søger gennem Java Virtual Machine Specification (JVM Spec), vil du ikke være i stand til at finde nogen sætning, der kommanderer, Hver JVM skal have en affaldssamler. Java Virtual Machine Specification giver VM-designere et stort spillerum til at beslutte, hvordan deres implementeringer styrer hukommelsen, herunder at beslutte, om de overhovedet skal bruge affaldsindsamling. Det er således muligt, at nogle JVM'er (såsom et JVM-kort med bare knogler) kan kræve, at programmer, der udføres i hver session, "passer" i den tilgængelige hukommelse.

Selvfølgelig kan du altid løbe tør for hukommelse, selv på et virtuelt hukommelsessystem. JVM Spec angiver ikke, hvor meget hukommelse der er tilgængelig for en JVM. Det hedder bare, at når en JVM gør løber tør for hukommelse, skal det kaste en OutOfMemoryError.

Ikke desto mindre vil de fleste JVM'er bruge en affaldssamler for at give Java-applikationer den bedste chance for at udføre uden at løbe tør for hukommelse. Affaldssamleren genvinder den hukommelse, der er optaget af objekter, der ikke er henvist til, på bunken, så hukommelsen kan bruges igen af ​​nye genstande og normalt fragmenterer bunken, når programmet kører.

Affaldsopsamlingsalgoritme er ikke defineret

En anden kommando, du ikke finder i JVM-specifikationen, er Alle JVM'er, der bruger affaldsindsamling, skal bruge XXX-algoritmen. Designerne af hver JVM får beslutte, hvordan affaldsindsamling fungerer i deres implementeringer. Affaldssamlingsalgoritme er et område, hvor JVM-leverandører kan stræbe efter at gøre deres implementering bedre end konkurrentens. Dette er vigtigt for dig som Java-programmør af følgende grund:

Fordi du generelt ikke ved, hvordan affaldsindsamling udføres inde i en JVM, ved du ikke, hvornår et bestemt objekt bliver indsamlet skrald.

Og hvad så? spørger du måske. Årsagen til, at du måske er ligeglad, når en genstand er indsamlet skrald, har at gøre med slutbehandlere. (EN slutbehandler er defineret som en almindelig Java-instansmetode med navnet færdiggør () der returnerer ugyldige og tager ingen argumenter.) Java-specifikationerne giver følgende løfte om færdiggørere:

Inden genindvinding af hukommelsen, der er optaget af et objekt, der har en finalizer, vil affaldssamleren påberåbe sig objektets finalizer.

I betragtning af at du ikke ved, hvornår genstande bliver indsamlet skrald, men du ved, at genstande, der kan færdiggøres, vil blive færdiggjort, da de skraldes, kan du foretage følgende store fradrag:

Du ved ikke, hvornår objekter skal færdiggøres.

Du skal indprente denne vigtige kendsgerning på din hjerne og altid lade den informere dine Java-objektdesign.

Færdiggørere, der skal undgås

Den centrale tommelfingerregel vedrørende færdiggørere er denne:

Design ikke dine Java-programmer, så korrektheden afhænger af "rettidig" afslutning.

Med andre ord, skriv ikke programmer, der går i stykker, hvis visse objekter ikke er færdige med bestemte punkter i programmets levetid. Hvis du skriver et sådant program, fungerer det muligvis på nogle implementeringer af JVM, men mislykkes på andre.

Stol ikke på færdiggørere for at frigive ressourcer, der ikke er hukommelse

Et eksempel på et objekt, der bryder denne regel, er et, der åbner en fil i dens konstruktør og lukker filen i dens færdiggør () metode. Selvom dette design virker pænt, ryddeligt og symmetrisk, skaber det potentielt en snigende bug. Et Java-program har generelt kun et begrænset antal filhåndtag til rådighed. Når alle disse håndtag er i brug, kan programmet ikke åbne flere filer.

Et Java-program, der bruger et sådant objekt (et, der åbner en fil i sin konstruktør og lukker det i sin finalizer) fungerer muligvis fint på nogle JVM-implementeringer. Ved sådanne implementeringer vil finalisering forekomme ofte nok til at holde et tilstrækkeligt antal filhåndtag til rådighed til enhver tid. Men det samme program kan mislykkes på en anden JVM, hvis affaldssamler ikke færdiggøres ofte nok til at forhindre, at programmet løber tør for filhåndtag. Eller hvad er endnu mere snigende, programmet fungerer muligvis på alle JVM-implementeringer nu, men mislykkes i en missionskritisk situation et par år (og frigiver cyklusser) nede ad vejen.

Andre tommelfingerregler

To andre beslutninger, der er overladt til JVM-designere, vælger den tråd (eller tråde), der udfører finalisatorerne, og rækkefølgen, i hvilken finalisatorer køres. Finaliserere kan køres i en hvilken som helst rækkefølge - sekventielt af en enkelt tråd eller samtidigt af flere tråde. Hvis dit program på en eller anden måde afhænger af rigtigheden af, at finalisatorer køres i en bestemt rækkefølge eller af en bestemt tråd, fungerer det muligvis på nogle JVM-implementeringer, men mislykkes på andre.

Du skal også huske på, at Java betragter et objekt som færdiggjort, om færdiggør () metoden vender tilbage normalt eller fuldføres pludseligt ved at kaste en undtagelse. Affaldssamlere ignorerer alle undtagelser fra finaliseringsprogrammer og underretter på ingen måde resten af ​​applikationen om, at en undtagelse blev kastet. Hvis du har brug for at sikre, at en bestemt finalizer fuldt ud udfører en bestemt mission, skal du skrive finalisatoren, så den håndterer alle undtagelser, der måtte opstå, inden finalisatoren fuldfører sin mission.

En tommelfingerregel om færdiggørere vedrører genstande, der er tilbage på bunken i slutningen af ​​applikationens levetid. Som standard vil affaldssamleren ikke udføre slutbehandlingen af ​​nogen genstande, der er tilbage på bunken, når applikationen afsluttes. For at ændre denne standard skal du påberåbe sig runFinalizersOnExit () metode til klasse Kørselstid eller System, passerer rigtigt som den enkelte parameter. Hvis dit program indeholder objekter, hvis finaliserere absolut skal påberåbes inden programmet afsluttes, skal du sørge for at påberåbe sig runFinalizersOnExit () et eller andet sted i dit program.

Så hvad er finalisatorer gode til?

Nu kan du måske få en fornemmelse af, at du ikke har meget brug for færdiggørere. Selvom det sandsynligvis er, at de fleste af de klasser, du designer, ikke inkluderer en finalizer, er der nogle grunde til at bruge finalizers.

En rimelig, men sjælden, ansøgning om en finalizer er at frigøre hukommelse, der er allokeret med native metoder. Hvis et objekt påberåber sig en indfødt metode, der tildeler hukommelse (måske en C-funktion, der kalder malloc ()), kunne objektets finalizer påberåbe sig en native metode, der frigør hukommelsen (opkald ledig()). I denne situation bruger du finalisatoren til at frigøre hukommelse, der er allokeret på vegne af et objekt - hukommelse, der ikke automatisk genvindes af affaldssamleren.

En anden, mere almindelig anvendelse af finaliseringsmaskiner er at tilvejebringe en reservemekanisme til frigivelse af endelige ressourcer, der ikke er hukommelse, såsom filhåndtag eller sockets. Som tidligere nævnt bør du ikke stole på, at finalisatorer frigiver endelige ressourcer, der ikke er hukommelse. I stedet skal du angive en metode, der frigiver ressourcen. Men du kan også ønske at medtage en finalizer, der kontrollerer for at sikre, at ressourcen allerede er frigivet, og hvis den ikke har det, fortsætter den og frigiver den. En sådan afsluttende afskærmning beskytter mod (og forhåbentlig ikke tilskynder til) sjusket brug af din klasse. Hvis en klientprogrammerer glemmer at påberåbe den metode, du har angivet for at frigive ressourcen, frigiver finalisatoren ressourcen, hvis objektet nogensinde bliver indsamlet skrald. Det færdiggør () metode til LogFileManager klasse, vist senere i denne artikel, er et eksempel på denne form for finalizer.

Undgå misbrug af finalizer

Eksistensen af ​​færdiggørelse giver nogle interessante komplikationer for JVM'er og nogle interessante muligheder for Java-programmører. Hvad finalisering tildeler programmører er magt over objekternes liv og død. Kort sagt er det muligt og fuldstændigt lovligt i Java at genoplive genstande i finalisatorer - at bringe dem tilbage til livet ved at gøre dem henvist til igen. (En måde en finalizer kan opnå dette er ved at tilføje en henvisning til objektet, der afsluttes, til en statisk linket liste, der stadig er "live.") Selvom en sådan magt kan være fristende at udøve, fordi det får dig til at føle dig vigtig, tommelfingerreglen er at modstå fristelsen til at bruge denne magt. Generelt udgør genoplivning af objekter i slutbehandlere misbrug af slutbehandler.

Den vigtigste begrundelse for denne regel er, at ethvert program, der bruger opstandelse, kan redesignes til et lettere forståeligt program, der ikke bruger opstandelse. Et formelt bevis for denne sætning efterlades som en øvelse for læseren (jeg har altid ønsket at sige det), men i en uformel ånd skal du overveje, at genstanden fra opstanden vil være lige så tilfældig og uforudsigelig som genstandens færdiggørelse. Som sådan vil et design, der bruger opstandelse, være vanskeligt at finde ud af den næste vedligeholdelsesprogrammerer, der sker sammen - som muligvis ikke fuldt ud forstår idiosynkrasierne ved affaldsindsamling i Java.

Hvis du føler, at du simpelthen skal bringe et objekt tilbage til livet, skal du overveje at klone en ny kopi af objektet i stedet for at genoplive det samme gamle objekt. Begrundelsen bag dette råd er, at affaldssamlere i JVM påberåber sig færdiggør () metode til et objekt kun en gang. Hvis objektet genopstår og bliver tilgængeligt til affaldsopsamling en anden gang, er objektet det færdiggør () metode påberåbes ikke igen.