tifyty

pure Java, what else ?

Gyengék vagyunk, puhák vagyunk, vagy nem is vagyunk

Egy korábbi bejegyzésben megnéztük, hogy hogyan néz ki a ThreadLocalMap és láttuk, hogy az Entry rész osztály kiterjeszti a WeakReference osztályt, de nem ástunk bele mélyebben, hogy mi is az. És hát főleg nem másztunk bele abba, hogy mi is az a SoftReference és pláne a PhantomReference.

A legtöbb, amit még gyakorlott és sokat (de nem eleget) látott Java programozóktól hallottam az, hogy ezek olyan referencia osztályok, amin keresztül el lehet érni objektumokat, addig, amíg a GC azokat össze nem gyűjti, és ezek a referenciák nem gátolják meg a GC-t, hogy összegyűjtse a hivatkozott objektumokat.

Azon túl, hogy ez az ismeret elég halvány, és maximum ahhoz használható, hogy ne lepődjünk meg, és szaladgáljunk sikoltozva, mint egy kislány, aki egeret látott a kamrában, amikor meglátjuk ezen osztályok valamelyikét egy keretrendszerben, de még csak nem is igaz: a fantom referencián keresztül nem lehet elérni az objektumot. (He? Se nem gátolja meg, hogy a GC összegyűjtse, se el nem érhető rajta keresztül, akkor meg minek van? Na majd meglátjuk jól.)

Mik ezek a referenciák?

Mielőtt ezekbe beleásunk először azt nézzük meg, hogy egyáltalán mi is az a Reference interfész amit mind a három referencia osztály implementál? Nos ez egy olyan interfész, amelyik egy másik objektumra való hivatkozás kezelését teszi lehetővé. Négy metódusa van:

  • clear()
  • enqueue()
  • get() és
  • isEnqueued()

A hivatkozott objektumot beállítani nem lehet, azt az implementációk konstruktor argumentumának kell átadni. A clear() törli a referenciát, a get() pedig visszaadja a hivatkozott objektumot (vagy nem). Ami igazán érdekes az az enqueue(). Ezt a metódust meghívhatjuk magunk is, de valószínűleg nem mi fogjuk meghívni, hanem a futtató VM. Legalábbis a puha, gyenge és fantom referenciák esetében amikor egy hivatkozott objektumot összegyűjtött és ledarált a GC, akkor a JVM meghívja az objektumra hivatkozó Reference objektum enqueue() metódusát. És ilyenkor a referenciát (már nem) hordozó objektum (nem amit a GC már begyűjtött, mert ugye, az már nincs is) bele fog kerülni egy ReferenceQueue-ba. Már, ha ilyent átadtunk a referencia létrehozásakor a konstruktornak. És abba, amelyiket átadtuk a létrehozás során.

Ilyen módon kaphatunk értesítést arról, hogy egy korábban létrehozott objektumot a rendszer begyűjtött és ledarált.

A puha és a gyenge referencia esetén van olyan konstrutor, amelyiknek csak a hivatkozott objektumot adjuk át paraméterként. A fantom referencia esetében csak olyan van, ahol a hivatkozott objektum mellett egy referencia sort is megadunk. Másnak ugyanis nem nagyon lenne értelme. A fantom referencia ugyanis null értéket ad vissza a get() meghívásakor, még akkor is ha a hivatkozott objektumunk vígan teszi a dolgát, és még sejtése sincs arról, hogy egyszer majd eléri a végzet.

És akkor most, hogy már kicsit belekapkodtunk, nézzük meg szisztematikusan, hogy mi is a három referencia típus, és utána azt is, hogy melyik mire jó. Majd pedig a végén örvendezzünk, hogy milyen jó, hogy mindezzel csak nagyon ritkán kell foglalkoznunk, mert hogy a keretrendszerek írói helyettünk már foglalkoztak ezekkel a dolgokkal!

A SoftReference egy olyan referencia, amelyiknek a get() metódusa visszaadja a hivatkozott objektumot, ha az még létezik, és a GC a hivatkozott objektumot garantáltan nem fogja begyűjteni amíg van rá legalább puha referencia, csak akkor, ha enélkül OutOfMemoryError lenne. Ebben az esetben viszont a puha referenciák egy részét, vagy mindet figyelmen kívül hagyja, és a hivatkozott objektumokat a GC ledarálja. Természetesen, ha a puha referencián kívül van rendes, erős referencia az objektumra, akkor a GC nem fogja kihúzni alólunk az objektumot.

A WeakReference is hasonló, de ebben az esetben a GC-t nem érdekli, hogy van-e fenyegető OutOfMemoryError lehetőség vagy nincs. Begyűjti (vagy nem) a hivatkozott objektumot (saját belátása szerint), amennyiben nincs rá élő puha vagy erős referencia pontosan úgy, mintha nem is lenne rá gyenge referencia.

Mind a két referencia esetében a get() metódus null értéket ad vissza, ha az objektum be lett gyűjtve és már nem létezik.

A PhantomReference abban különbözik a gyenge referenciától, hogy a get() metódus akkor is null értéket ad vissza, ha az objektum még él. (Ezt mondtam már, azt hiszem.)

A ReferenceQueue referenciákat ad vissza. Három publikus metódusa van, amelyek arra szolgálnak, hogy megnézzük, hogy van-e benne elem, illetve, hogy kivegyünk belőle egy referenciát.

Mire valók ezek a referenciák?

Soft Reference

A puha referencia tipikusan arra való, hogy objektumokat cache-ben tartsunk, de ha nincs elég hely, akkor ne akadályozzuk meg, hogy a GC kidobálja ezeket az objektumokat. Vagy nem?

Hát sok mindent lehet csinálni, például péppé lehet verni anyóst golfütővel, de ahogy a golfütőt sem arra találták ki, a puha referencia sem erre való. Ennek ellenére mégis erre használják nagyon sokan, de erről majd egyszer talán egy másik poszt. Most csak annyit, hogy a cache elemek élettartamát ne bízzuk a véletlenre.

No de akkor mire való a puha referencia? Nézzük meg a következő példát:

package com.verhas.puha;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
import junit.framework.TestCase;
import org.junit.Test;
public class PuhaVagyJeno extends TestCase {
	private boolean weNeedIt() {
		return true;
	}
	private String fetchStringFromStore() throws UnsupportedEncodingException {
		return new String( new byte[100000],"utf-8");
	}
	@Test
	public void testOutOrMemoryError() throws Exception {
		List<String> results = new LinkedList<String>();
		while (weNeedIt()) {
			results.add(fetchStringFromStore());
		}
	}
}

A weNeedIt() és a fetchStringFromStore() helyébe a való életben képzeljünk valami olyant, ami valamilyen adatbázisból vagy máshonnan olvas adatot, és időről időre előfordulhat, hogy olyan sok sort akar beolvasni, amire már nincs elég memória. Mi történik? Természetesen elfogy a memória. Mit lehet tenni? Maximum elkapjuk az OutOfMemoryError-t, de ezzel az erővel a programunk az AIDS-et is elkaphatja: hogyan kezeled, amikor éppen nincs is memória?

Természetesen akkor sem lesz végtelen memóriánk, ha ügyesen puha referenciát használunk, de legalább lesz esélyünk kezelni a helyzetet:

package com.verhas.puha;
import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.List;
import junit.framework.TestCase;
import org.junit.Test;
public class MegPuhabbVagyJeno extends TestCase {
	private boolean weNeedIt() {
		return true;
	}
	private String fetchStringFromStore() throws UnsupportedEncodingException {
		return new String(new byte[100000], "utf-8");
	}
	@Test
	public void testOutOrMemoryError() throws Exception {
		SoftReference<List<String>> ref = new SoftReference<List<String>>(
				new LinkedList<String>());
		List<String> results;
		while (weNeedIt()) {
			String s = fetchStringFromStore();
			results = ref.get();
			if (results == null) {
				// ooops... no memory, our linked list was already released
				return;
			} else {
				results.add(s);
			}
			results = null;
		}
	}
}

Mi történik? Amikor elfogy a memória, és megpróbálja lefoglalni a memóriát az új String számára, beindul a GC és felszabadítja a láncolt listánkat. Két fontos dolgot kellett a kódban átalakítani, azon kívül, hogy a puha referenciát használjuk:

Az egyik, hogy a results változót lenullázzuk. A GC ugyanis nem tudná ledarálni a láncolt listát ha az erős referenciánk a results változóban megmarad. Ez még akkor is igaz lenne, ha a results változó szkópjából kilépünk. A GC ugyanis nem tud róla, hogy egy lokális változó még elérhető-e, látható-e a metódus éppen futó részében. Ez Java szintű dolog, a GC meg JVM szinten, bájt kódon fut, és ez nem látszik a bájt kódban, ez nyelvi elem.

A másik, hogy az új string létrehozását ki kellett emelni egy olyan sorra, ahol a results értéke éppen null.

Így már nem kell elkapnunk az OutOfMemoryError-t. Jeleztük a puha referenciával, hogy mit dobjon ki a léghajóból (asszony marad, anyós megy), és amikor kidobta, akkor nem hibát dob hanem széttárja a kezét és hiába kérjük tőle a jó kis láncolt listánkat halkan csak annyit mond: null. A láncolt listádnak annyi lett, öregem. Viszont van elég heap-ed, hogy megjeleníts egy hibaüzenetet a felhasználó felé: “próbálkozz máskor, amikor kevesebben használják az App szervert és több memória jut a lekérdezésedre.”

Weak Reference

A gyenge referencia használatára már láttunk példát a ThreadLocalMap osztályban. Egy olyan map-et hozott létre, amelyikben meg lehetett keresni egy objektumot, de nem akartuk, hogy a GC csak azért ne tudja ledarálni az objektumot, mert benne van a map-ban. Ha meghalt, meghalt. A golfütőt jól eldugjuk, és túltesszük magunkat rajta.

Ami még érdekes lehet ebben a témakörben, hogy ilyen map-et nem kell nekünk magunknak implementálni: van ilyen. Úgy hívják, hogy HashMap. Hé!!! Nem WeakHashMap-et akartál írni?

Bizony nem!

A WeakHashMap egy olyan map, amelyik a kulcs objektumokra tart gyenge referenciát. Ha az értékekre kell gyenge referencia, akkor HashMap helyett HashMap-t kell használni. És persze oda kell figyelni, amikor egy olyan referenciát kapunk vissza, amelyiknek a get() metódusa null-t ad vissza.

A WeakHashMap azonban nem ilyen egyszerű, az nem egy HashMap. Az ugyanis referencia objektumokat használna kulcsnak. A WeakHashMap viszont a mi eredeti objektumainkat használja kulcsként, csak éppen nem tart rájuk erős vagy puha referenciát. Csak gyengét, és ha a referencia mögül eltűnt a hivatkozott objektum, akkor ki is takarítja a bejegyzést.

Phantom Reference

Na ez a legérdekesebb. Mire való a fantom referencia? Se meg nem védi a hivatkozott objektumot a begyűjtéstől, se el nem lehet kérni tőle. Nos akkor mire való?

Arra, amire egyébként a puha és a gyenge referenciát is lehet használni, hogy a programunk értesítést kapjon arról, amikor egy objektumot a rendszer begyűjt. Ehhez létre kell hoznunk egy ReferenceQueue-t, és amikor létrehozzuk a referenciát, akkor a konstruktorban meg kell adnunk ezt a referencia sor példányt:

package com.verhas.fantomas;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashSet;
import java.util.Set;
public class Fantomas {
	public static void main(String[] argv) throws Exception
	{
	    Set<PhantomReference<byte[]>> refs = new HashSet<PhantomReference<byte[]>>();
	    ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
	    for (int i = 0 ; i < 10000 ; i++)
	    {
	        PhantomReference<byte[]> ref
	            = new PhantomReference<byte[]>(new byte[100000], queue);
	        System.err.println(i + ": created " + ref);
	        refs.add(ref);
	        Reference<? extends byte[]> r2;
	        while ((r2 = queue.poll()) != null)
	        {
	            System.err.println("cleared " + r2);
	        }
	    }
	}
}

Ez egy nagyon egyszerű mintapélda. Nagyon sok, százezer bájt hosszú tömböt hozunk létre, és csak fantom referenciákat tartunk meg ezekre az objektumokra. Ebben a pillanatban ezek az objektumok azonnal begyűjthetővé is válnak, és egy idő után a GC be is fogja gyűjteni őket.

A referencia létrehozásakor átadunk a konstruktornak egy ReferenceQueue sort, és amikor a begyűjtés megtörtént, akkor a fantom referenciák megjelennek a queue sorban.

Ha ezt a kis programot elindítjuk mondjuk egy

java version "1.7.0_07"
Java(TM) SE Runtime Environment (build 1.7.0_07-b10)
Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode)

rendszeren -Xmx12m paraméterrel (amelyik igencsak aprócska heap méretet enged meg), akkor látni fogjuk, hogy a létrehoz elemeket, majd pedig egyszer csak egy csomagban megjelennek ezek a referencia elemek a queue-ban, utána fut tovább a program, megint létrejönnek elemek, majd megint beindul a GC és minden elem felszabadul, és kb. 100 és 200 cikluslefutás között egyszer csak beüt az OutOfMemoryError. Ennek az az oka, hogy a fantom referenciák is elfoglalnak valamennyi helyet, és ezeket nem dobtuk el. De miért tettük bele a fantom referenciákat a Set-be? Nem használjuk sehol.

De igen: arra használjuk, hogy a fantom referencia objektumokra legyenek erős referenciáink, és a GC ne dobja ki a fantom referencia objektumokat a hivatkozott objektumokkal együtt. Ha ugyanis ezt tenné, akkor a GC után/során nem lenne amit a queue-ba bele tudna rakni, és nem is rakná bele. Viszont ha már belekerült a queue-ba és ki is vettük onnan, akkor ki lehet venni a Set-ből is egy

refs.remove(r2);

sorral, és ezzel nagy valószínűséggel elkerüljük a memória elfogyását. (Azért 12MB heap-pel nagyon sokat ne reméljünk. Hol van már a ZX Spektrum 48KB, vagy a 640KB mindenre elég!)

És akkor mielőtt nagyon boldogok lennénk a fantom referenciákkal, le kell, hogy lombozzam a lelkesedést: nincs rá garancia, hogy a fantom referencia belekerül a queue-ba. Csak akkor fog belekerülni, ha a hivatkozott objektumot a GC ledarálta. Ebből a szempontból pontosan ugyanolyan helyzetben vagyunk, mint finalize metódussal, amire eleddig még egyetlen értelmes példát sem láttam, hogy mire lehetne használni (marad a van, de ne piszkáljuk).

De ha valaki mégis talál ilyen feladatot, akkor valószínűleg jobban jár, ha nem az objektum finalize metódusát hívja, hanem inkább kezel (akár aszinkron szálon) egy ReferenceQueue-t. Miért?

  • Finalizer egy olyan objektumon fut, amit a szemétgyűjtő már kijelölt szemétgyűjtésre, és olyan állapotban van, amelyik … szóval már nem nagyon kellene vele semmit csinálni.
  • Ha véletlenül sikerül egy új erős referenciát létrehozni erre az objektumra, akkor az objektumot nem tudja begyűjteni a GC, de ettől még nem kerül vissza élő állapotba az objektumunk: zombi lesz. Ha újra elfogy minden erős referencia, akkor a GC megtalálja és ledarálja, de már nem fogja újra meghívni a finalizer-t.
  • A JVM sokszor egy szálon, egymás után futtatja a finalizer metódusokat, de ha van is több szál, akkor is, az fogja, de legalábbis befolyásolja a GC-t.
  • Amikor a finalizer fut, akkor tipikusan kevés a memória, hiszen elindult a GC, és mi másért indult el, mint azért, mert kevés a memória.

Ha azonban van egy referencia sorunk, akkor ezek a problémák nincsenek.

Összefoglalás

Ez egy eléggé száraz, hosszú, és tömény cikk lett. Ebből is látszik, hogy nem szeszes ital, mert abban ami tömény, az rövid. Viszont körbejártunk valamit, ami tapasztalatom szerint sokak számára nem világos.

A bejegyzést a cikk alapján írtam. Az eredeti cikkben vannak részek, amelyeket itt nem részleteztem, néhány példát leegyszerűsítettem, és vannak olyan témák (az se egy rövid cikk) amelyeket itt kihagytam.

És cikkírás közben is sokat tanulok.

8 responses to “Gyengék vagyunk, puhák vagyunk, vagy nem is vagyunk

  1. Botond január 2, 2013 3:29 du.

    Világos volt eddig is, de csak azért, mert korábban már belefutottam a dologba. A fantom-referenciáknak épp a szerző miatt néztem anno utána kicsit tüzetesebben 😀

  2. Gábor Lipták január 3, 2013 10:16 de.

    Saját tapasztalatom arra amit írtál (lehetőleg ne legyen a cache SoftReference) ebben a bugreportban olvasható: http://java.net/jira/browse/PDF_RENDERER-151 . PDFRenderer softreference cache öldökli a GC-t.

  3. tamasrev január 7, 2013 2:09 de.

    Fhu, ez annyira érdekes, lassan kedvem lesz keretrendszert írni.

  4. Visszajelzés:final, fin ül « tifyty

  5. Visszajelzés:Objektum Internálás | tifyty

  6. Sipi december 26, 2016 1:10 du.

    Csak egy apróság az egyébként remek cikkhez: “először azt nézzük meg, hogy egyáltalán mi is az a Reference interfész”
    Az 1.2-es és 1.7-es JDK szerint is ez nem interfész, hanem absztrakt osztály.

  7. Sipi december 26, 2016 11:15 du.

    Viszont van még egy dolog ami problémás. A jVisualVM-mel elemeztem a példakód futását és ha ott van az a results = null; ha nincs, mindkét esetben megtörténik a memóriafelszabadítás és semmiféle OutOfMemory exception nem is jön! Na most azt nem értem, hogy a ciklusmagban hol szűnhet meg az erős referencia hatásköre? Mert látszólag sehol, de mégis lefut a GC és kipucolja a listát. (1.7.0_51-es JDK)
    Viszont kipróbáltam ennek a két sornak a felcserélését:
    String s = fetchStringFromStore();
    results = ref.get();

    És ha nem ebben a sorrendben vannak, akkor attól függetlenül, hogy a results = null; ott van-e, mindenképpen jön az OOM exception. (Ezt írod is lejjebb, tehát rendben is van.)
    A results=null a tesztjeim szerint akkor kell, ha a ciklus előtt értéket adunk neki, a deklarációval együtt. Ez fura, mert ezek szerint ha ezt nem tesszük meg, akkor a JVM minden egyes iteráció kezdetekor úgy veszi, hogy null, függetlenül attól, hogy az előző végén már mutatott valahová?
    Szóval itt valami varázslat van, amit nem értek.

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: