Správa paměti v Javě s integrovaným tříděním odpadu je jedním z nejlepších výdobytků tohoto jazyka. Umožňuje vývojářům vytvářet nové objekty, aniž by se museli výslovně starat o alokaci a dealokalizaci paměti, protože garbage collector automaticky získává zpět paměť pro opětovné použití. To umožňuje rychlejší vývoj s menším množstvím šablonovitého kódu a zároveň eliminuje úniky paměti a další problémy související s pamětí. Alespoň teoreticky.
Ironicky se zdá, že kolekce smetí v Javě funguje příliš dobře a vytváří a odstraňuje příliš mnoho objektů. Většina problémů se správou paměti je vyřešena, ale často za cenu vzniku vážných výkonnostních problémů. Přizpůsobení garbage collection všem druhům situací vedlo ke vzniku složitého a těžko optimalizovatelného systému. Abyste se v kolekci smetí vyznali, musíte nejprve pochopit, jak funguje správa paměti ve virtuálním stroji Java (JVM).
Jak kolekce smetí v Javě skutečně funguje
Mnoho lidí si myslí, že kolekce smetí sbírá a zahazuje mrtvé objekty. Ve skutečnosti dělá kolekce smetí v Javě pravý opak! Sledují se živé objekty a vše ostatní se označuje jako odpad. Jak uvidíte, toto zásadní nepochopení může vést k mnoha problémům s výkonem.
Začněme u haldy, což je oblast paměti používaná pro dynamické přidělování. Ve většině konfigurací operační systém předem alokuje haldu, kterou spravuje JVM za běhu programu. To má několik důležitých důsledků:
- Vytváření objektů je rychlejší, protože není nutná globální synchronizace s operačním systémem pro každý jednotlivý objekt. Alokace jednoduše nárokuje určitou část paměťového pole a posune ukazatel offsetu dopředu (viz obrázek 2.1). Další alokace začíná na tomto offsetu a nárokuje si další část pole.
- Když se objekt již nepoužívá, garbage collector získá zpět podkladovou paměť a znovu ji použije pro budoucí alokaci objektů. To znamená, že nedochází k explicitnímu mazání a operačnímu systému se nevrací žádná paměť.
Obrázek 2.1: Nové objekty jsou jednoduše alokovány na konci použité haldy.
Všechny objekty jsou alokovány v oblasti haldy spravované JVM. Takto se zachází s každým prvkem, který vývojář používá, včetně objektů tříd, statických proměnných a dokonce i samotného kódu. Dokud je na objekt odkazováno, považuje jej JVM za živý. Jakmile na objekt již není odkazováno, a není tedy pro kód aplikace dosažitelný, garbage collector jej odstraní a získá zpět nevyužitou paměť. Jakkoli to zní jednoduše, vyvolává to otázku: Jaký je první odkaz ve stromu?“
Kořeny kolekce odpadků – zdroj všech stromů objektů
Každý strom objektů musí mít jeden nebo více kořenových objektů. Dokud se aplikace může dostat k těmto kořenům, je celý strom dosažitelný. Kdy jsou však tyto kořenové objekty považovány za dosažitelné? Speciální objekty nazývané kořeny sběru odpadků (kořeny GC; viz obrázek 2.2) jsou vždy dosažitelné a stejně tak každý objekt, který má ve svém kořeni kořen sběru odpadků.
V Javě existují čtyři druhy kořenů GC:
- Lokální proměnné jsou udržovány při životě zásobníkem vlákna. Nejedná se o skutečnou virtuální referenci objektu, a proto není viditelná. Pro všechny záměry a účely jsou lokální proměnné kořeny GC.
- Aktivní vlákna Javy jsou vždy považována za živé objekty, a proto jsou kořeny GC. To je důležité zejména pro lokální proměnné vláken.
- Statické proměnné jsou odkazovány svými třídami. Tato skutečnost z nich de facto činí GC kořeny. Třídy samotné mohou být garbage-collected, což by odstranilo všechny odkazované statické proměnné. To má zvláštní význam, pokud používáme aplikační servery, OSGi kontejnery nebo obecně zavaděče tříd. Související problémy probereme v části Problémové vzory.
- Reference JNI jsou objekty jazyka Java, které nativní kód vytvořil v rámci volání JNI. S takto vytvořenými objekty se zachází speciálně, protože JVM neví, zda na ně nativní kód odkazuje, nebo ne. Takové objekty představují velmi zvláštní formu kořenů GC, kterou se budeme podrobněji zabývat v části Problémové vzory níže.
Obrázek 2.2: GC kořeny jsou objekty, na které se JVM sám odkazuje, a které tak brání každému jinému objektu ve sběru odpadků.
Jednoduchá aplikace v Javě má tedy tyto GC kořeny:
- Místní proměnné v metodě main
- Hlavní vlákno
- Statické proměnné třídy main
Značení a zametení smetí
Pro určení, které objekty se již nepoužívají, spouští JVM přerušovaně algoritmus, který se velmi výstižně nazývá mark-and-sweep. Jak asi tušíte, jde o přímočarý proces o dvou krocích:
- Algoritmus prochází všechny reference na objekty, počínaje kořeny GC, a každý nalezený objekt označí jako živý.
- Všechna paměť haldy, která není obsazena označenými objekty, je rekultivována. Je jednoduše označena jako volná, v podstatě se zbaví nepoužívaných objektů.
Sběr odpadu má odstranit příčinu klasických úniků paměti: nedosažitelné, ale neodstraněné objekty v paměti. Funguje však pouze pro úniky paměti v původním smyslu. Je možné mít nepoužívané objekty, které jsou pro aplikaci stále dosažitelné, protože je vývojář jednoduše zapomněl dereferencovat. Takové objekty nemohou být shromažďovány jako odpad. Ještě horší je, že takový logický únik paměti nemůže žádný software odhalit (viz obrázek 2.3). I ten nejlepší analytický software dokáže na podezřelé objekty pouze upozornit. Analýzou úniku paměti se budeme zabývat níže v části Analýza vlivu využití paměti a garbage collection na výkon.