JE_05: Odstra�te zastaralé odkazy na objekty

Když přejdete z jazyka s manuální správou paměti, jakými jsou C nebo C++, k jazyku s automatickým uvol�ováním paměti, pak máte jako programátor mnohem snazší úlohu, protože vaše objekty se automaticky odstraní, jakmile práci s nimi ukončíte. Když si o poprvé vyzkoušíte, zdá se to být jako nějaká kouzla. Snadno si pak můžete myslet, že na správu paměti vůbec nemusíte pomýšlet, což však není úplně pravda.

Zvažme následující implementaci jednoduchého zásobníku:

// Dokážete najít "paměťový únik"?
public class Stack {
	private Object[] elements;
	private int size = 0;

	public Stack(int initialCapacity) {
		this.elements = new Object[initialcapacity];
	}

	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}

	public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		return elements[--size];
	}

	
	/**
	 * Zajistíme prostor přinejmenším pro jeden další prvek, přičemž
	 * přibližně zdvojnásobíme kapacitu, kdykoli se má pole zvětšit.
	 */
	private void ensureCapacity() {
		if (elements.length == size) {
			Object[] oldElements = elements;
			elements = new Object[2 * elements.length + 1];
			System.arrayCopy(oldElements, 0, elements, 0, size);
		}
	}
}

Na tomto programu není na první pohled nic špatného. Můžete je detailně otestovat a každým testem bravurně projde, přesto z něj však vykukuje jeden problém. Volně řečeno, program má "paměťový únik", který se může potichu projrvovat omezenou výkonností způsobenou vyšší aktivitou nástroje uvol�ování paměti nebo zvýšenou spotřebou paměti. V extrémních případech mohou takové paměťové úniky způsobovat odkládání dat na disk a dokonce i selhání programu s chybou OutOfMemoryError. Taková selhání však nastávají relativně zřídkakdy.

Tak kudy tedy paměť uniká? Pokud se zásobník zvětší a následně zmenší, pak nedojde k odstranění vyloučených objektů zásobníku z paměti, třebaže se na ně program využívající takový zásobník již neodkazuje. Je to proto, že zásobník zachovává zastaralé odkazy na tyto objekty. Zastaralý odkaz je prostě odkaz, který již nikdy nebude zrušen. v tomto případě jsou zastaralé všechny odkazy mimo "aktivní část" pole prvků. Aktivní část se skládá z prvků, jejichž index je menší než size.

Paměťové úniky (přesněji označované za neúmyslná zachovávání objektů) jsou v jazycích s automatickým uvol�ováním paměti velmi nepříjemné. Dojde-li k neúmyslnému zachování odkazu na nějaký objekt, pak nejenže se z paměti daný objekt neodstraní, ale neodstraní se ani žádné další objekty odkazované tímto objektem atd. I když dojde k neúmyslnému zachování jen několika málo odkazů na objekty, nemusí se z paměti uvolnit mnoho a mnoho dalších objektů, což může mít velký dopad na výkonnost.

Náprava tohoto problému je jednoduchá: Stačí vynulovat odkazy, jakmile jsou zastaralé. V případě naší třídy Stack se stane určitý odkaz na položku zastaralým, jakmile je odstraněn ze zásobníku. Opravená verze metody pop vypadá takto:

	public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		Object result = elements[--size];
		elements[size] = null; // Odstranění zastaralého odkazu
		return result;
	}

Další výhodou odstranění zastaralých odkazů je to, že pokud se na ně budete chybně později znovu odkazovat, programokamžitě skončí s chybou NullPointerException a neudělá tedy tiše nesprávnou věc. Vždy je vhodné odhalovat chyby programování co nejdříve.

Když se programátoři poprvé setkají s podobným problémem, pak mají tendenci starat se až příliš a vymazávat každý odkaz na objekt, jakmile jej program nepotřebuje. To není ani nutné, ani žádoucí, protože se program zbytečně komplikuje a může docházet ke snížení výkonnosti. Vymazávaní odkazů na objekty by mělo být výjimkou a nikoli normou. Nejlepším způsobem eliminace zastaralého odkazu je opakovaně použít proměnnou, v níž byl obsažen, nebo ji nechat vypadnout z oboru platnosti. K tomu pochopitelně dojde, když budete definovat každou proměnnou v nejužším možném oboru platnosti (rada 29). Stojí za připomenutí, že v současných implementacích JVM nestačí jen ukončit blok, v němž je daná proměnná definovaná; je zapotřebí opustit také obklopující metodu,a by daný odkaz zmizel.

Kdy byste tedy měli vymazat nějaký odkaz? Jaký aspekt třídy Stack ji vystavuje paměťovým únikům? Jednoduše řečeno, třída Stack spravuje svou vlastní paměť. Úložná sada se skádá z prvků pole items (buněk odkazů na objekty, nikoli objektů samotných). Prvky v aktivní části pole (jak byla dříve definována) jsou alokované a prvky ve zbývající čáasti pole jsou volné. To však mechanismus uvol�ování paměti nemůže vědět; pro něj jsou všechny odkazy na objekty v poli items stejně platné. Pouze programátor ví, že neaktivní část pole je nedůležitá. Programátor vlastně tuto skutečnost oznámí nástroji uvol�ování paměti tím, že vymaže prvky pole, jakmile se stanou součástí neaktivní části.

Obecně platí, že kdykoli si třída spravuje svou vlastní paměť, musí si programátor dávat pozor na paměťové úniky. Kdykoli dojde k uvolnění nějakého prvku, je zapotřebí vymazat všechny odkazy na objekty obsažené v tomto prvku.

Dalším obecným zdrojem paměťových úniků jsou mezipaměti (cache). Jakmile vložíte do mezipaměti odkaz na nějaký objekt, snadno zapomenete na jeho existenci a ponecháte jej v mezipaměti i dlouho poté, kdy již není zapotřebí. Existují dvě možná řešení tohoto problému. Pokud máte štěstí a implementujete mezipaměť, v níž je položka relevantní přesně tak dlouho, dokud existují odkazy na její klíč mimo mezipaměť, pak mezipaměť reprezentujte jako WeakHashMap; zadání se automaticky odstraní, jakmile se stanou zastaralými. Obvyklejší je však to, že doba, po kterou je zadání v mezpaměti relevantní, není dobře definována a hodnota zadání klesá s časem. V takových případech je zapotřebí z mezipaměti občas vymazat zadání, která ji jen zneužívají. Toto čištění lze vykonávat vláknem na pozadí (třeba pomocí API java.util.Timer) nebo jako vedlejší efekt přidávání nových zadání do mezipaměti. Třída java.util.LinkedHashMap přidaná ve verzi 1.4 usnad�uje tento druhý přístup svo metodou removeEldestEntry.

Protože paměťové úniky se obvykle neprojevují jako zřejmé chyby, mohou existovat v systému mnoho let. Obvykle dojde k jejich odhalení jen při podrobné kontrole kódu nebo s pomocí ladícího nástroje nazývaného profilátor hromady. Proto je nanejvýš vhodné naučit se očekávat podobné potíže, ještě než se projeví, a vyhýbat se jim.