dagblog.
← Archiv

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.

Verze na Medium

Související články

Související epizody CZ Podcastu