úterý 26. července 2005

Memory leaks v Jave

Memory leaks jsou problémem, který vzniká tak, že program konsumuje operační paměť, kterou neumožňuje uvolnit, i když ji už v danou chvíli nepotřebuje. V Jave se o uvolnění paměti stará Garbage collector (GC) též česky řečený Sběrač Neplatných Objektů, který je plně v režii Java Virtual Machine. Programátor se nemusí starat o uvolňování paměti ručně jako například v C.

Bohužel i v javovských programech dochází k memory leaks, které končí hlášením Out of Memory. Pro objasnění příčiny memory leaks je potřeba objasnit zjednodušeně práci GC a paměťový model Javy. Základ paměťového modelu tvoří zásobník (stack) a halda (heap). Jakýkoliv objekt, který se v Jave vytvoří, leží na haldě.

 new Object(); //na haldě vznikne nový objekt

Pokud v Jave vytvoříme proměnou, vznikne záznam na zásobníku.

 Object o; //na zásobníku vznikne prázdný záznam, na který odkazuje proměnná o

Když to celé spojíme do jednoho kousku kódu Object o = new Object();, pak máme proměnou o, která odkazuje na záznam v zásobníku. Voláním konstruktoru vznikne objekt na haldě. Operátor přiřazení pak zajistí to, že do záznamu na zásobníku se dostane paměťový odkaz na objekt alokovaný na haldě.

Pokud na objekt, který leží na haldě, ukazuje záznam ze zásobníku, považuje se tento objekt za živý. Pokud se na objekt ze zásobníku neukazuje, považuje se objekt za mrtvý a GC jej může odstranit z haldy, tedy dojde k uvolnění paměti. Pro ilustraci si vezmeme náš případ a vsadíme jej do obyčejné metody.

 
  class Foo{
   void doSomething(){
      Object o = new Object();
   } 
 }
 

Při volání metody doSomething nám na zásobníku vznikne záznam, ve kterém je uložen paměťový odkaz na objekt alokovaný na haldě. Po skončení metody, nám zanikne lokální proměnná o a tím pádem i záznam na zásobníku. Na haldě teď leží objekt, na který neukazuje žádný záznam ze zásobníku.

Ve chvíli kdy JVM vyhodnotí, že je potřeba uvolnit paměť spustí GC. GC pak "proleze" celý zásobník a všechny objekty na haldě, na které je odsuď ukazováno, označí za živé. Všechny ostatní pak může GC odstranit a tím hromadně uvolnit paměť.

Zvažme následující příklad, kdy máme třídu, která je zodpovědná za vrácení rodného čísla zaměstnance.

 
  public class RodnaCisla{
   private static Map cisla = new HashMap();
    
   public String getRC(Integer id){      
      String rc = cisla.get(id);
      if(rc == null) {
       rc = ...//získej číslo např. z DB
        cisla.put(id, rc); 
      }      
      return rc; 
   } 
 }
 

Tahle třída neděla nic jiného, než že čísla zaměstnanců, která již byla získána, uloží do statické mapy. Tím pádem se s každým požadavkem na vrácení rodného čísla, která již bylo jednou nalezeno, nemusí šahat do databáze, ale vrátí se z této mapy.

Z pohledu paměťového modelu máme na zásobníku proměnou cisla, která je statická (sdílí se pro všechny instance třídy RodnaCisla) a vznikne při zavedení třídy RodnaCisla ClassLoaderem. S každým požadavkem na vrácení rodného čísla, které ještě nebylo získáno, se tohle číslo mimo jiné uloží do mapy, na kterou odkazuje cisla.

Postupem času se na haldě začnou hromadit objekty, které jsou odkazované z této statické mapy a GC je nemůže odklidit, neboť jsou pořád živé. Neustálým hromaděním nových a nových rodných čísel dochází k postupnému zabírání paměti, která nebude uvolněna a dochází k tzv. memory leaku.

Tento triviální příkládek demonstruje jak nesprávná implementace cacheovaní může způsobit memory leak, který se potom velice pracně dohledává technikou zvanou profiling. Pro detekci memory leak se používá nástroj, kterému se říká profiler. Při profilingu se JVM spustí ve speciálním módu, kdy přes standardizované rozhraní poskytuje profileru informace, na základě kterých je možné memory leak detekovat.

Tímto dlouhým oslím můstkem jsem udělal cestu k článku Staffana Larsena Memory Leaks, Be Gone.