2012. január 21., szombat

Weak és Soft referenciák a Javaban

Több éves Java tapasztalattal a hátam mögött hallottam először a gyenge (weak) referenciák létezéséről a nyelvben, és úgy gondoltam érdemel pár szót a téma kör. Javaslom, akinek nem világos a JVM szemétgyűjtőjének (továbbiakban GC) működése, ezen a ponton olvassa el Viczián István idevágó bejegyzését. Tehát mint tudjuk a GC kidob a memóriából minden olyan objektumot, melyre nem mutat már egyetlen referencia sem. Amikor létrehozunk egy objektumot, Integer foo = new Integer(0), akkor a foo egy strong reference lesz rá, és amíg az objektum "strongly reachable", addig a GC-nek tabu. Mivel ezeket az objektumokat nem tudja a GC felszabadítani, a memória beteltével jön a fránya OutOfMemotyError, és az alkalmazás kilép. Egy darabig persze lehet növelni a memóriát, majd a fizikai határok elérésével lehet elosztani az alkalmazást, de előbb álljunk meg egy szóra! Vajon minden objektumra szükségünk van a memóriában? Kézenfekvő megoldás, hogy bizonyos objektumokat azonnal megszüntessünk, amint nincs rájuk szükség, viszont az objektumok újbóli létrehozása is nagy költség, ebben az esetben pedig értékes processzoridőt fecsérelünk az állandó memóriaallokációra, példányosításra, stb. Arany középútként az 1.2-es, igen az 1.2-es Java verzióban bevezették a weak referenciákat, melynek implementációja a WeakReference osztály. A WeakReference referenciát tárol az adott objektumra, annyi különbséggel, hogy ezt az objektumot a GC első futáskor, szó nélkül eltávolítja a memóriából, felszabadítva ezzel a helyet mások számára.

WeakReference<StringBuilder> weakString = new WeakReference<StringBuilder>(new StringBuilder());

int i = 0;
while (weakString != null && weakString.get() != null) {
 weakString.get().append(i++);
 System.out.println(i);
}

System.out.println("Finish");

Kezdetként futási paraméterként állítsuk be a JVM-nek a maximális memóriát mondjuk 2 Mb-ra (-Xmx2m), majd az értékkel játszva láthatjuk, hogy mennyit változik programunk kimenete. Ez a technika kiválóan alkalmas memória gyorsítótárak készítésére, ám amennyiben kulcs-érték-pár alapú gyorsítótárat készítünk a new WeakReference(myMapInstance) helyett használjuk, az erre a célra készített WeakHashMap implementációt. A WeakHashMap a kulcsokat őrzi weak referenciával, és a MapEntry automatikusan eltávolításra kerül, amikor a kulcs már nincs rendszeres használat alatt.

A WeakReference mellett létezik a SoftReference osztály is, amely azt garantálja, hogy az OutOfMemoryError előtt felszámolásra kerülnek az objektumok, helyet biztosítva az újonan létrejövőknek.

SoftReference softObj = new SoftReference(new Serializable() {
 @Override public void finalize() throws Throwable {
  System.out.print("Finalize runned");

  super.finalize();
 } 
});

StringBuilder sb = new StringBuilder("foobar");
while (true) {
  sb.append(sb.toString());
}
A kimenetben láthatjuk, hogy miközben az alkalmazás akkut memóriahiányban elhalálozott, még utolsó leheletével felszámolta osztályunkat.
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Finalize runned at java.util.Arrays.copyOf(Arrays.java:2882)
 at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
 at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390)
 at java.lang.StringBuilder.append(StringBuilder.java:119)
 at com.blogspot.jpattern.Main.run(Main.java:36)
 at com.blogspot.jpattern.Main.main(Main.java:22)
Java Result: 1
Természetesen ennél complexebb program esetében az alkalmazás tovább tud dolgozni a felszabadult memóriával.A Java specifikáció szerint semmi garancia nincs a finalize() metódus futására!

Evezzünk egy kicsit sötétebb vizekre. Ha jól megnézzük a Reference API dokumentációját, láthatjuk, hogy van még egy ismert implementáció, a PhantomReference. Az első különbség társaihoz képest, hogy a bele helyezett objektumra soha nem tudunk referenciát kérni, ugyanis konstans null értékkel válaszol a get() hívására. Másik nagy eltérés lényege tömören, hogy az ilyen objektumokat a GC csak azelőtt rendezi sorba, mielőtt a fizikai memóriából kitakarítaná. Mivel úgy tudom Darth Vader nagyurat is ennek használata állította át az erő sötét oldalára, én magam nem merészkedtem ennél tovább (esetleg írhatnátok valami konkrét használati esetet).

Előfordulhat, hogy az alkalmazásunkban szükséges tudni, hogy mely objektumokat dobta már ki a GC, és melyeket nem. Ilyen esetben egy ReferenceQueue osztályt kell példányosítanunk, és a queue példányt átadni a WeakReference vagy SoftReference konstruktorának. A bejegyzésben említett referenciákkal óvatosan bánjunk, és csak alapos tervezés és tesztelést követően alkalmazzuk éles bevetésben őket.

5 megjegyzés:

  1. Helló!
    A SoftReference-nél nem pontos a megfogalmazásod és a példaprogram is rossz irányba húz. A SoftReference-nek az a lényege, hogy amikor kevés kezd lenni a memória, akkor kezdi el felszabadítani, így esetleg megelőzhető az OutOfMemoryError. (Cache implementációkra kiváló.) A példaprogramban minden határon túl növeled a StringBuilder méretét, így szegény JVM hiába szabadítja fel a SoftReference-eket, a végén így is elfogy a memóriája. Inkább az lenne jó példaporgram, hogy csinálsz jó sok SoftReferenced objektumot, aztán szép lassan növelsz egy StringBuilder-t, és figyeled hogy a softreference-k hogyan paterolódnak ki a memóriából.
    tvk

    VálaszTörlés
  2. Köszi szépen az észrevételt, valóban a megfogalmazás egy kicsit félrement (javítottam). A példaprogramot szándékosan írtam egyszerűre, hiszen csak azt hivatott bemutatni, hogy a GC kihajítoja az objektumot a memóriából OME előtt.

    VálaszTörlés
  3. Ez miért van?



    -mx 10M: 16107
    -mx 11M: 236953 !!!
    -mx 12M: 18157

    import java.lang.ref.WeakReference;

    public class TestWeakRef {

    public static void main(String... args){
    WeakReference weakString = new WeakReference(new StringBuilder());

    int i = 0;
    while (weakString != null && weakString.get() != null) {
    weakString.get().append(i++);
    System.out.println(i);
    }

    System.out.println("Finish");
    }
    }

    e:\projektek\java\weakreference>java -version
    java version "1.6.0_31"
    Java(TM) SE Runtime Environment (build 1.6.0_31-b05)
    Java HotSpot(TM) Client VM (build 20.6-b01, mixed mode, sharing)

    VálaszTörlés
  4. Séróból nyilván nem megy a válasz, és eddig nem volt időm tesztelni a a saját gépemen. Rajta vagyok, de plz adj egy kis időt.

    VálaszTörlés
  5. Megpróbáltam reprodukálni, nálam nem jelentkezett az anomália :( javaslom visualvm-mel kapcsolódj rá futás közben hátha többminden látszódik.

    VálaszTörlés