Programování

Dokončení a vyčištění objektu

Před třemi měsíci jsem zahájil minisérii článků o navrhování objektů s diskusí o principech návrhu, která byla zaměřena na správnou inicializaci na začátku života objektu. V tomhle Techniky návrhu článku se zaměřím na principy návrhu, které vám pomohou zajistit správné vyčištění na konci životnosti objektu.

Proč uklidit

Každý objekt v programu Java používá výpočetní prostředky, které jsou konečné. Je zřejmé, že všechny objekty používají určitou paměť k ukládání svých obrázků na hromadu. (To platí i pro objekty, které deklarují žádné proměnné instance. Každý obraz objektu musí obsahovat nějaký druh ukazatele na data třídy a může obsahovat i další informace závislé na implementaci.) Ale objekty mohou kromě paměti používat i jiné omezené prostředky. Některé objekty mohou například používat prostředky, jako jsou úchyty souborů, grafické kontexty, sokety atd. Když navrhujete objekt, musíte se ujistit, že nakonec uvolní všechny omezené prostředky, které používá, aby systému tyto prostředky nedocházelo.

Vzhledem k tomu, že Java je jazyk, který shromažďuje odpadky, je uvolnění paměti spojené s objektem snadné. Vše, co musíte udělat, je pustit všechny odkazy na objekt. Protože se nemusíte starat o výslovné uvolnění objektu, jak je to nutné v jazycích, jako je C nebo C ++, nemusíte si dělat starosti s poškozením paměti náhodným uvolněním stejného objektu dvakrát. Musíte se však ujistit, že skutečně uvolníte všechny odkazy na objekt. Pokud tak neučiníte, můžete skončit s únikem paměti, stejně jako únik paměti, který získáte v programu C ++, když zapomenete explicitně uvolnit objekty. Nicméně, pokud uvolníte všechny odkazy na objekt, nemusíte si dělat starosti s výslovným „uvolněním“ této paměti.

Podobně si nemusíte dělat starosti s výslovným uvolněním jakýchkoli základních objektů, na které odkazují proměnné instance objektu, který již nepotřebujete. Uvolněním všech odkazů na nepotřebný objekt se ve skutečnosti zruší platnost všech odkazů na jednotlivé objekty obsažené v proměnných instance daného objektu. Pokud byly nyní zneplatněné odkazy jedinými zbývajícími odkazy na tyto základní objekty, budou tyto základní objekty k dispozici také pro uvolňování paměti. Kus koláče, že?

Pravidla sběru odpadu

Ačkoli uvolňování paměti skutečně dělá správu paměti v Javě mnohem jednodušší než v C nebo C ++, nemůžete při programování v Javě úplně zapomenout na paměť. Chcete-li vědět, kdy možná budete muset přemýšlet o správě paměti v Javě, musíte vědět něco o tom, jak je ve specifikacích Java zacházeno s ukládáním odpadu.

Sběr odpadu není nařízen

První věc, kterou je třeba vědět, je, že bez ohledu na to, jak pilně prohledáváte specifikaci Java Virtual Machine (JVM Spec), nebudete moci najít žádnou větu, která vám přikazuje, Každý JVM musí mít garbage collector. Specifikace Java Virtual Machine poskytuje návrhářům virtuálních počítačů velkou volnost při rozhodování o tom, jak jejich implementace budou spravovat paměť, včetně rozhodování, zda vůbec či vůbec nepoužít sběr odpadků. Je tedy možné, že některé JVM (například JVM čipové karty s holými kostmi) mohou vyžadovat, aby se programy provedené v každé relaci „vešly“ do dostupné paměti.

Samozřejmě vám může vždy dojít paměť, dokonce i na systému virtuální paměti. Specifikace JVM neuvádí, kolik paměti bude k dispozici JVM. Pouze uvádí, že kdykoli JVM dělá došla paměť, mělo by to hodit OutOfMemoryError.

Nicméně, aby aplikace Java měly největší šanci na spuštění bez vyčerpání paměti, většina JVM použije sběrač odpadků. Sběrač odpadků uvolňuje paměť obsazenou neodkazovanými objekty na haldě, takže paměť může být znovu použita novými objekty a obvykle de-fragmentuje haldu při spuštění programu.

Algoritmus odvozu odpadu není definován

Další příkaz, který ve specifikaci JVM nenajdete, je Všechny JVM, které používají sběr odpadků, musí používat algoritmus XXX. Návrháři každého JVM se mohou rozhodnout, jak bude při jejich implementacích fungovat sběr odpadků. Algoritmus sběru odpadu je jednou z oblastí, ve které se prodejci JVM mohou snažit, aby jejich implementace byla lepší než u konkurence. To je pro vás jako programátora Java významné z následujících důvodů:

Protože obecně nevíte, jak bude sběr odpadků prováděn uvnitř JVM, nevíte, kdy bude odebrán nějaký konkrétní objekt.

No a co? můžete se zeptat. Důvod, proč by vás mohlo zajímat, když je objekt shromažďován, souvisí s finalizátory. (A finalizátor je definována jako běžná metoda instance Java s názvem dokončit() který vrací prázdnotu a nepřijímá žádné argumenty.) Specifikace Java slibují ohledně finalizátorů následující slib:

Před rekultivací paměti obsazené objektem, který má finalizátor, uvolňovač paměti vyvolá finalizátor daného objektu.

Vzhledem k tomu, že nevíte, kdy budou objekty shromažďovány odpadky, ale víte, že finalizovatelné objekty budou finalizovány, protože jsou shromažďovány odpadky, můžete provést následující velký odpočet:

Nevíte, kdy budou objekty finalizovány.

Tuto důležitou skutečnost byste měli vtisknout do svého mozku a navždy mu umožnit informovat vaše návrhy objektů Java.

Finalizátory, kterým je třeba se vyhnout

Ústředním pravidlem týkajícím se finalizátorů je toto:

Nenavrhujte své programy Java tak, aby správnost závisela na „včasné“ finalizaci.

Jinými slovy, nepište programy, které se rozbijí, pokud určité objekty nebudou dokončeny v určitých bodech životnosti provádění programu. Pokud takový program napíšete, může fungovat na některých implementacích JVM, ale na jiných selže.

Při uvolňování nepaměťových prostředků se nespoléhejte na finalizátory

Příkladem objektu, který porušuje toto pravidlo, je ten, který otevře soubor v jeho konstruktoru a zavře soubor v jeho dokončit() metoda. Ačkoli se tento design jeví jako čistý, uklizený a symetrický, potenciálně vytváří zákeřnou chybu. Program Java obecně bude mít k dispozici pouze konečný počet popisovačů souborů. Pokud se všechny tyto popisovače používají, program nebude schopen otevřít další soubory.

Program Java, který takový objekt využívá (takový, který otevírá soubor v konstruktoru a zavírá ho ve svém finalizátoru), může u některých implementací JVM fungovat dobře. U takových implementací by finalizace proběhla dostatečně často, aby byl vždy k dispozici dostatečný počet popisovačů souborů. Stejný program však může selhat na jiném JVM, jehož sběrač odpadků se nedokončí dostatečně často, aby programu nedocházelo úchyty souborů. Nebo, co je ještě zákeřnější, program může nyní fungovat na všech implementacích JVM, ale selže v kritické situaci několik let (a cykly vydání).

Další pravidla finalizátoru

Dvě další rozhodnutí ponechaná návrhářům JVM jsou výběr podprocesů (nebo podprocesů), které budou provádět finalizátory, a pořadí, ve kterém budou finalizátory spuštěny. Finalizátory mohou být spuštěny v jakémkoli pořadí - postupně jedním vláknem nebo současně více vlákny. Pokud váš program nějak závisí na správnosti finalizátorů spouštěných v určitém pořadí nebo konkrétním vláknem, může fungovat na některých implementacích JVM, ale na jiných selže.

Měli byste také mít na paměti, že Java považuje objekt za dokončený, zda dokončit() metoda se vrátí normálně nebo se náhle dokončí vyvoláním výjimky. Garbage collectors ignorují všechny výjimky vyvolané finalizátory a v žádném případě neoznámí zbytku aplikace, že byla vyvolána výjimka. Pokud potřebujete zajistit, aby určitý finalizátor plně splnil určitou misi, musíte tento finalizátor napsat tak, aby zpracovával všechny výjimky, které mohou nastat, než finalizátor dokončí svou misi.

Jedno další pravidlo o finalizátorech se týká objektů ponechaných na haldě na konci životnosti aplikace. Ve výchozím nastavení garbage collector nespustí finalizátory žádných objektů ponechaných na haldě při ukončení aplikace. Chcete-li toto výchozí nastavení změnit, musíte vyvolat runFinalizersOnExit () metoda třídy Runtime nebo Systémprojíždějící skutečný jako jediný parametr. Pokud váš program obsahuje objekty, jejichž finalizátory musí být absolutně vyvolány před ukončením programu, nezapomeňte je vyvolat runFinalizersOnExit () někde ve vašem programu.

K čemu jsou tedy finalizátory dobré?

Nyní už můžete mít pocit, že finalizátory příliš nepoužíváte. I když je pravděpodobné, že většina tříd, které navrhnete, nebude obsahovat finalizátor, existuje několik důvodů pro použití finalizátorů.

Jedna rozumná, i když vzácná aplikace pro finalizátor, je uvolnění paměti přidělené nativními metodami. Pokud objekt vyvolá nativní metodu, která přiděluje paměť (možná funkce C, která volá malloc ()), finalizátor tohoto objektu by mohl vyvolat nativní metodu, která uvolní tuto paměť (volání volný, uvolnit()). V této situaci byste použili finalizátor k uvolnění paměti přidělené jménem objektu - paměti, která nebude automaticky uvolněna garbage collectorem.

Dalším, běžnějším použitím finalizátorů je poskytnout záložní mechanismus pro uvolnění konečných prostředků, které nejsou v paměti, jako jsou popisovače souborů nebo sokety. Jak již bylo zmíněno dříve, neměli byste se spoléhat na finalizátory pro uvolňování konečných jiných než paměťových prostředků. Místo toho byste měli poskytnout metodu, která uvolní prostředek. Můžete ale také chtít zahrnout finalizátor, který zkontroluje, zda byl zdroj již vydán, a pokud se tak nestane, pokračuje a uvolní jej. Takový finalizátor chrání před (a doufejme, že nepodporuje) nedbalé používání vaší třídy. Pokud klientský programátor zapomene vyvolat metodu, kterou jste zadali k uvolnění prostředku, finalizátor uvolní prostředek, pokud je objekt někdy shromažďován odpadky. The dokončit() metoda LogFileManager třída, ukázaná dále v tomto článku, je příkladem tohoto druhu finalizátoru.

Vyhněte se zneužití finalizátoru

Existence finalizace vytváří některé zajímavé komplikace pro JVM a některé zajímavé možnosti pro programátory Java. Programátorům poskytuje finalizace moc nad životem a smrtí objektů. Stručně řečeno, v Javě je možné a zcela legální vzkřísit objekty ve finalizátorech - přivést je zpět k životu tím, že je znovu odkazujeme. (Jedním ze způsobů, jak toho může finalizátor dosáhnout, je přidání odkazu na finalizovaný objekt do statického propojeného seznamu, který je stále „živý“.) Ačkoli taková síla může být lákavá k výkonu, protože vám dává pocit důležitosti, je odolat pokušení použít tuto sílu. Obecně vzkříšení objektů ve finalizátorech představuje zneužití finalizátoru.

Hlavním důvodem tohoto pravidla je, že jakýkoli program, který používá vzkříšení, může být přepracován do lépe srozumitelného programu, který nepoužívá vzkříšení. Formální důkaz této věty je ponechán jako cvičení pro čtenáře (vždy jsem to chtěl říci), ale v neformálním duchu zvažte, že vzkříšení objektu bude stejně náhodné a nepředvídatelné jako finalizace objektu. Jako takový bude design, který využívá vzkříšení, těžko přijatelný pro příštího programátora údržby, který se stane spolu - kdo nemusí plně pochopit idiosynkrázy odvozu odpadu v Javě.

Pokud máte pocit, že musíte jednoduše přivést objekt zpět k životu, zvažte klonování nové kopie objektu namísto vzkříšení stejného starého objektu. Důvodem této rady je, že sběratelé odpadu v JVM vyvolávají dokončit() metoda objektu pouze jednou. Pokud je tento objekt vzkříšen a stane se k dispozici pro uvolnění paměti podruhé, objekt je dokončit() metoda nebude znovu vyvolána.