Slabé a ještě slabší reference v Javě
Dnes se podíváme na jedno z méně známých zákoutí Javy, na slabší formy referencí na objekty - WeakReference a její příbuzní. Mohou se hodit třeba v situacích, když řešíte memory leak nebo se mu snažíte předejít.
Problematika, do které se dnes pustíme, není tak populární jako kolekce nebo streamy a existuje mnoho javistů, kteří se s ní navzdory několikaleté praxi nikdy nepotýkali. Důvod je nasnadě; vývoj "běžných" aplikací totiž často řeší stále dokola tytéž problémy a tam kde je poptávka, vznikne i nabídka. A tak používáme knihovny a frameworky, které nás odstiňují od nudných opakujících se činností a my se tak můžeme soustředit na implementaci business logiky, hezčí architekturu, nebo lepší pokrytí automatizovanými testy. Mnozí z nás se tak nikdy nepotkali kupříkladu se synchronizací a obecně s vlákny (threads), nebo právě se slabými referencemi a jejich variantami.
Není to střelba do vlastních řad, jen holé konstatování skutečnosti. Také mám rád pohodlí a nechci psát pro každou malou službu psát třeba HTTP server, spravovat si thread pooly, connections, a celé síťové I/O. Je třeba si ale přiznat, že si často ani neuvědomujeme, jak obrovskou komplexitu delegujeme na udělátka, která napsal někdo jiný a že jim ani do hloubky nerozumíme. Jsou to pro nás více či méně blackboxy, ktere za nás dělají "tu magii" a nám stačí třeba přidat anotaci. Někdy ale dojde na lámání chleba a je potřeba si trochu ušpinit ruce. Toho se my, medvědi, samozřejmě nebojíme a nebojíme se o to ani podělit :-).
Za vším hledej paměť
Abychom pochopili, co to znamená, že na nějaký objekt existuje slabá reference, je potřeba si alespoň rámcově uvědomit, jak v Javě (resp. na JVM obecně - týká se to tedy i Kotlinu, Scaly a dalších) vlastně funguje správa paměti. Pro většinu tohle nebude novinkou, ale malé opáčko neuškodí.
Na rozdíl třeba od C/C++, kde má programátor nad alokováním a uvolňováním paměti takřka neomezenou moc, ale i s tím související plnou odpovědnost. V Javě naproti tomu prostě děláme nepořádek a máme k dispozici "úklidovou četu", která to po nás uklidí. Jmenuje se garbage collector (GC) a je to mechanismus unvnitř JVM, který na pozadí hledá paměť, kterou naše aplikace sice stále zabírá, ale už jí ke svému běhu nepotřebuje. Sbírá smetí, tedy nepotřebné objekty. Nabízí se otázka, jak se v paměti pozná užitečný objekt od nepotřebného - a tím se vracíme k naší problematice referencí.
Zjednodušeně řečeno, potřebný objekt je takový, na který se nějaký jiný potřebný objekt odkazuje (drží na něj referenci). Jinými slovy, objekt je dosažitelný z jiného objektu. Pak se předpokládá, že jej bude chtít využít, a proto není možné jej z paměti (zatím) odstranit. Kdykoli tak ve svém objektu vytvoříte instanci nějaké třídy a uložíte si na ní referenci (zavoláte "new" a výsledek uložíte do proměnné), vytvořený objekt bude "žít", dokud bude "žít" váš objekt (nebo dokud se nezbavíte reference, např. nastavením na null, popřípadě přepsáním původní proměnné na jinou hodnotu). Držením reference tak zabráníte garbage collectoru v uvolnění paměti.
Většinou je to naprosto v pořádku a žádoucí, někdy to ale může být na obtíž, zvlášť jedná-li se o objemná data, která jsou navíc třeba dočasného charakteru. Naštěstí má pro tyto případy JVM konstrukt, který tento problém řeší. Vedle běžné - silné - reference (strong reference) máme totiž k dispozici několik slabších variant. Všechny tyto varianty dědí od společného předka java.lang.ref.Reference a všechny spojuje ta vlastnost, že odkazovaný objekt již nemusí být k dispozici. Implementují metodu get(), která vrátí odkazovaný objekt (hodnotu reference), popřípadě null, pokud jste přišli s křížkem po funuse.
Silné, slabé, měkké a přízraky
Nebojte se, doslovných překladů se už vyvarujeme :-) Podívejme se, jaké možnosti nám platforma nabízí. Nejsou seřazené podle síly vazby (v příkladech dole už ano), ale tak, aby se dobře popisovaly rozdíly mezi nimi.
Strong reference
S přehledem nejběžnější reference, používá ji každý programátor, i kdyby o tom nevěděl. Jak již bylo zmíněno výše, jedná se o běžné přiřazení nějakého objektu do proměnné. Zároveň je to jediný typ vazby, který bere garbage collector opravdu vážně. Objekty, na které vedou silné reference z jiných objektů nemohou být uvolněny z paměti, jsou považovány za potřebné pro běh aplikace. Celá problematika je o něco složitější, garbage collector detekuje a odstraňuje například "islands of isolation", tedy objekty navzájem propojené silnými referencemi, ale jinak nedosažitelné z aktivních částí aplikace a vůbec je mnohem "chytřejší", než jak by mohlo z tohoto příspěvku vyznít. Po zbytek textu se ale držme tohoto zjednodušení.
Weak reference
Jedná se o referenci, která není dostatečně silná na to, aby zabránila ostranění objektu z paměti. Garbage collector této referenci nepříkládá význam a budou pro něj pak rozhodující jen ostatní reference směřující na daný objekt. Jakmile se objeví objekt, který je "svázán" s ostatními pouze prostřednictvím těchto referencí, měl by být odstraněn. Weak reference jsou ještě poměrně známé díky tomu, že je nad nimi postavena často používaná WeakHashMap.
Soft reference
O něco málo silnější vazba je soft reference, garbage collector by měl být teoreticky trochu zdrženlivější a s odstraňováním počkat, až to bude nutné (až bude paměti nedostatek). Nejedná se ale o závazné chování, jen o doporučení, většina JVM ale bude odstraňovat objekty dosažitelné jen přes soft reference později, než objekty dosažitelné jen přes weak reference. Pořadí, ve kterém se uvolňují také není závazné a může se lišit podle konkrétního JVM. Je ale doporučováno, aby se preferovalo odstraňování starších nebo dlouho nepoužitých referencí. Přímo garantováno je však pouze to, že budou odstraněny veškeré tyto reference (a objekty, které jsou dosažitelné pouze tímto typem reference) dříve, než VM vyhodí out of brambory (OutOfMemoryError). Soft reference se často používají např. pro implementaci nějaké cache.
Phantom reference
Nejslabší z nejslabších, slabší než vlákno z pavučiny. Reference je tak slabá, že přes ní původní objekt vůbec nelze získat, get() metoda vrací natvrdo null. Pozorný čtenář (naivně doufám alespoň ve dva čtenáře, z nichž jeden by mohl být pozorný) se jistě ptá, k čemu je to dobré? Pravdou je, že se používají spíše okrajově, ale své využití mají. Aby to ale dávalo smysl, musíme se zmínit ještě o jednom souvisejícím konceptu.
Reference queue
V překladu fronta referencí, a přesně to i dělá. Při vytváření kterékoli ze zmíněných typů referencí (kromě běžného přiřazení - strong reference) můžete určit i frontu, do které se budou reference vkládat po odstranění objektů, na které odkazují. Tím je možné se dozvědět, že referencované objekty už neexistují a nějak na to zareagovat. Právě phantom reference nemají žádný jiný smysl, než zpětnout "notifikaci" přes reference queue, ostatně proto třída PhantomReference narozdíl od odstatních nemá dva konstruktory - s reference queue a bez ní - ale pouze jeden.
Příklady použití
Pár příkladů, seřazeno od nejsilnější vazby po nejslabší:
`<p>CODE: https://gist.github.com/Duck28/05bec479bf9bc50b19cb7f5b63b661dc.js</p>`
Poznámka: příklad s reference queue je čistě ilustrační a velmi zjednodušený. Metoda poll() vrátí referenci na poslední objekt, který byl z paměti odstraněn z množiny referencí zaregistrovaných nad danou reference queue. V reálném případě se vrácená reference spíše použije k pročištění nějaké cache apod., přímé porovnání nedává moc smysl, pokud by do fronty bylo zaregistrováno více než jedna reference. V případě jedné by pak stačilo porovnávat, zda metoda poll() vrací nebo nevrací null.
Závěr
To by bylo prozatím asi vše. Naťukli jsme téma, které člověk nepotřebuje každý den, ale hodí se o něm mít alespoň povědomí. Až budete příště řešit záhadné memory leaky, třeba kvůli bugu v jinak užitečné knihovně, třeba si vzpomenete na tento článek a podaří se vám problém vyřešit rychle a bez zbytečného tápání. Budeme držet palce :-)