Vše, co potřebujete vědět o únicích paměti v systému Android.

Ali Asadi
30. června 2019 – 16 minut čtení

Jednou ze základních výhod Javy, nebo přesněji řečeno JVM (Java Virtual Machine), je garbage collector (GC). Můžeme vytvářet nové objekty, aniž bychom se museli starat o jejich uvolňování z paměti. O alokaci a uvolnění paměti se za nás postará garbage collector.

Ne tak docela! Můžeme zabránit tomu, aby za nás garbage collector paměť uvolnil, pokud plně nerozumíme tomu, jak GC funguje.

Psaní kódu bez dobrého porozumění tomu, jak GC funguje, může způsobit úniky paměti v aplikaci. Tyto úniky mohou ovlivnit naši aplikaci tím, že plýtvají neuvolněnou pamětí a nakonec způsobí výjimky z nedostatku paměti a zpoždění.

Co je to únik paměti?

Neuvolnění nepoužívaných objektů z paměti

Neuvolnění nepoužívaných objektů z paměti znamená, že v aplikaci existují nepoužívané objekty, které GC nemůže vymazat z paměti.

Když GC nemůže vymazat nepoužívané objekty z paměti, máme problém. Paměťová jednotka, v níž jsou nepoužívané objekty, bude obsazena až do konce aplikace nebo (do konce metody).

Do konce metody? Ano, je to tak. Máme dva druhy úniků, úniky, které zabírají paměťovou jednotku až do konce aplikace, a úniky, které zabírají paměťovou jednotku až do konce metody. První z nich je jasný. Druhý je třeba více objasnit. Pojďme si to vysvětlit na příkladu! Předpokládejme, že máme metodu X. Metoda X provádí na pozadí nějakou dlouhou běžeckou úlohu, jejíž dokončení bude trvat minutu. Metoda X při tom také drží nepoužívané objekty. V takovém případě bude paměťová jednotka obsazena a nepoužívané objekty nebude možné po dobu jedné minuty až do ukončení úlohy vymazat. Po ukončení metody může GC nepoužívané objekty vymazat a získat paměť zpět.

To je to, co chci, abyste prozatím věděli, že se k tomu vrátíme později pomocí kódu a vizualizace. BUDE TO ZÁBAVA. 👹😜

Počkejte chvilku!!!🥴

Než se vrhneme na souvislosti, začneme od základů.

RAM neboli paměť s náhodným přístupem je paměť v zařízeních nebo počítačích se systémem Android, která slouží k ukládání aktuálně spuštěných aplikací a jejich dat.

Vysvětlím dva hlavní znaky operační paměti, prvním je halda a druhým zásobník. Přejděme k té zábavnější části 🤩🍻.

Heap & Stack

Nebudu to příliš protahovat. Přejděme rovnou k věci, stručný popis: Zásobník se používá pro statické přidělování paměti, zatímco halda pro dynamické přidělování paměti. Jen mějte na paměti, že jak halda, tak zásobník jsou uloženy v paměti RAM.

Více o paměti haldy

Paměť haldy v Javě používá virtuální stroj k alokaci objektů. Kdykoli vytvoříte objekt, je vždy vytvořen v haldě. Virtuální stroje, jako je JVM nebo DVM, provádějí pravidelný garbage collection (GC), čímž zpřístupňují paměť haldy všech objektů, na které se již neodkazuje, pro budoucí alokace.

Pro zajištění bezproblémového uživatelského prostředí nastavuje systém Android pevný limit velikosti haldy pro každou spuštěnou aplikaci. Limit velikosti haldy se u jednotlivých zařízení liší a závisí na tom, kolik má zařízení paměti RAM. Pokud vaše aplikace narazí na tento limit haldy a pokusí se alokovat více paměti, obdrží hlášení OutOfMemoryError a ukončí se.

Přemýšleli jste někdy, jaká je velikost haldy pro vaši aplikaci?

Pojďme to společně zjistit. V systému Android máme virtuální počítač Dalvik (DVM). DVM je jedinečný virtuální stroj Java optimalizovaný pro mobilní zařízení. Optimalizuje virtuální stroj z hlediska paměti, výdrže baterie a výkonu a je zodpovědný za rozdělení množství paměti pro každou aplikaci.

Povíme si o dvou řádcích v DVM:

  1. dalvik.vm.heapgrowthlimit: Tento řádek je založen na tom, jak Dalvik začne ve velikosti haldy vaší aplikace. Je to výchozí velikost haldy pro každou aplikaci. Je to maximum, kterého může vaše aplikace dosáhnout!
  2. dalvik.vm.heapsize: Tento řádek představuje maximální velikost haldy pro větší haldu. Toho můžete dosáhnout tak, že v manifestu aplikace požádáte android o větší haldu (android:largeHeap=“true“).

Nepoužívejte ve své aplikaci větší haldu. Udělejte to POUZE v případě, že přesně znáte vedlejší efekt tohoto kroku. Zde vám poskytnu dostatek informací, abyste mohli pokračovat ve zkoumání tématu.

Tady je tabulka, která ukazuje, jakou velikost haldy jste získali na základě paměti RAM vašeho zařízení:

+==========================+=========+=========+===================+
| DVM | 1GB RAM | 2GB RAM | 3GB RAM OR HIGHER |
+==========================+=========+=========+===================+
| DEFAULT(heapgrowthlimit) | 64m | 128m | 256m |
+--------------------------+---------+---------+-------------------+
| LARGE(heapsize) | 128m | 256m | 512m |
+--------------------------+---------+---------+-------------------+

Pamatujte, že čím více ram máte, tím větší bude velikost haldy. mějte na paměti, že ne všechna zařízení s vyšší pamětí ram jdou nad 512 m. Udělejte si průzkum na svém zařízení, pokud má vaše zařízení více než 3 GB, abyste zjistili, zda je vaše velikost haldy větší než 512 m.

Jak můžete zkontrolovat velikost haldy aplikace pro vaše zařízení?

Pomocí ActivityManager. Maximální velikost haldy můžete za běhu zkontrolovat pomocí metod getMemoryClass() nebo getLargeMemoryClass() (když je povolena velká halda).

  • getMemoryClass():
  • getLargeMemoryClass(): Vraťte výchozí maximální velikost haldy:
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Jak to funguje v reálném světě?

Pomocí této jednoduché aplikace pochopíme, kdy používáme haldu a kdy zásobník.

aplikace

Následující obrázek ukazuje zobrazení haldy a zásobníku aplikace a místa, kam jednotlivé objekty ukazují a kam se ukládají, když aplikaci spustíme.

Projdeme si spuštění aplikace, zastavíme se u každého řádku, vysvětlíme, kdy aplikace alokuje objekty a ukládá je na haldu nebo zásobník. Ukážeme si také, kdy aplikace objekty ze zásobníku a haldy uvolňuje.

  • Řádek 1 – JVM vytvoří blok paměti zásobníku pro metodu main.

  • Řádek 2 – V tomto řádku vytvoříme primitivní lokální proměnnou. Proměnná bude vytvořena a uložena v paměti zásobníku hlavní metody.

  • Řádek 3 -Tady potřebuji vaši pozornost!!!! V tomto řádku vytvoříme nový objekt. Objekt je vytvořen na zásobníku metody main a uložen na haldě. V zásobníku je uložena reference, paměťová adresa (ukazatel) objektu a haldy, zatímco v haldě je uložen původní objekt.

  • Řádek 4 – Stejný jako řádek 3. V řádku 4 je uložen původní objekt.

  • Řádek 5 – JVM vytvoří blok paměti zásobníku pro metodu foo.

  • Řádek 6 -Vytvoření nového objektu. Objekt se vytvoří v paměti zásobníku metody foo, přičemž do zásobníku uložíme adresu paměti haldy objektu, který jsme vytvořili na řádku 3. Hodnotu (adresu paměti haldy objektu v řádku 3) jsme předali v řádku 5. Nezapomeňte, že Java vždy předává reference podle hodnoty.

  • Řádek 7 – Vytváříme nový objekt. Objekt vytvořený v zásobníku a ukázal na fond řetězců v haldě.

  • Řádek 8 – V posledním řádku metody foo se metoda ukončila. A objekty budou uvolněny ze zásobníkového bloku metody foo.

  • Řádek 9- Stejně jako řádek 8, V posledním řádku metody main se metoda ukončila. A blok zásobníku hlavní metody se uvolní.

Co s uvolněním paměti z haldy? K tomu se brzy dostaneme. Vezmi si kafe☕️ a pokračuj dál 😼.

Co se stane, když se metody ukončí?

Každá metoda má svůj vlastní obor. Když je metoda ukončena, objekty se automaticky uvolní a rekultivují ze zásobníku.

obrázek 1

Na obrázku 1, když se ukončí metoda foo. Paměť zásobníku nebo blok zásobníku metody foo se automaticky uvolní a rekultivuje.

obrázek 2

Na obrázku 2 totéž. Když se metoda main ukončila. Paměť zásobníku nebo blok zásobníku hlavní metody se automaticky uvolní a rekultivuje.

Závěr

Teď je nám jasné, že objekty v zásobníku jsou dočasné. Jakmile se metoda ukončí, objekty se uvolní a rekultivují.

Zásobník je datová struktura LIFO (Last-In-First-Out). Můžete se na něj dívat jako na krabici. Pomocí této struktury může program snadno spravovat všechny své operace pomocí dvou jednoduchých operací: push a pop.

Při každém uložení, například proměnné nebo metody, se posune ukazatel zásobníku nahoru. Pokaždé, když ukončíte metodu, vyskočí vše z ukazatele zásobníku až do návratu na adresu předchozí metody. V našem příkladu se vracíme z metody foo do metody main.

A co halda?

Halda je něco jiného než zásobník. Pro uvolňování a zpětné získávání objektů z paměti haldy potřebujeme pomoc.

Pro to nám Java, přesněji řečeno JVM, vytvořila superhrdinu, který nám pomáhá. Nazvali jsme ho Garbage Collector. Ten tu těžkou práci udělá za nás. A starat se o detekci nepoužívaných objektů, uvolnit je a získat zpět více místa v paměti.

Jak garbage collector funguje?“

Jednoduše. Sběrač odpadků hledá nepoužívané nebo nedosažitelné objekty. Pokud se na haldě nachází objekt, na který neukazuje žádný odkaz, postará se garbage collector o jeho uvolnění z paměti a získá zpět více místa.

GC

Kořeny GC jsou objekty, na které odkazuje JVM. Jsou to počáteční objekty stromu. Každý objekt ve stromu má jeden nebo více kořenových objektů. Dokud aplikace nebo kořeny GC mohou dosáhnout těchto kořenů nebo těchto objektů, je celý strom dosažitelný. Jakmile se stanou z aplikace nebo kořenů GC nedosažitelnými, budou považovány za nepoužívané objekty nebo nedosažitelné objekty.

Co se stane, když se spustí garbage collector?

Prozatím je to aktuální stav paměti aplikace. Zásobník je prázdný a halda je plná nepoužívaných objektů.

Před GC

Po spuštění GC budou výsledky následující:

Po GC

GC uvolní a vymaže všechny nepoužívané objekty z haldy.

Člověk! A co ten únik paměti, na který čekáme? LOL, ještě kousek Lil a budeme tam. V naší jednoduché aplikaci byla aplikace napsána skvěle a jednoduše. V kódu nebylo nic špatného, co by mohlo GC zabránit v uvolnění objektů haldy. A proto GC uvolní a znovu získá všechny objekty z paměti haldy. Pokračujte dále. V další části máme spoustu příkladů úniků paměti😃.

Kdy a jak dochází k únikům paměti?

K úniku paměti dochází, když zásobník stále odkazuje na nepoužívané objekty v haldě.

Na obrázku níže je jednoduché vizuální znázornění pro lepší pochopení tohoto pojmu.

Na vizuálním znázornění vidíme, že když máme objekty odkazované ze zásobníku, ale již nepoužívané. Sběrač odpadků je nikdy neuvolní ani neuvolní z paměti, protože ukazuje, že tyto objekty jsou používány, zatímco nejsou.

Jak můžeme způsobit únik paměti?

V systému Android můžeme únik paměti způsobit různými způsoby. A lze to snadno provést pomocí AsyncTasks, Handlerů, Singletonů, Vláken a dalších.

Ukážu několik příkladů s použitím vláken, singletonů a posluchačů, abych vysvětlil, jak můžeme způsobit únik paměti a jak se jim vyhnout a opravit je.

Podívejte se na můj repozitář Github. Mám tam několik příkladů kódu.

Jak můžeme způsobit únik pomocí vláken?

V tomto příkladu spustíme aktivitu, která na pozadí spustí vlákno. Vlákno bude provádět úlohu, jejíž dokončení trvá 20 sekund.

Jak známo, vnitřní třídy drží implicitní odkaz na svou obklopující třídu.

V pozadí vlastně vypadá aktivita takto.

Třída DownloadTask drží odkaz na třídu ThreadActivity.

Co se tedy stane po spuštění úlohy nebo vlákna?

Existují dva možné toky používání aplikace. Běžný tok, který funguje podle očekávání bez chyb, a tok úniku, který způsobí únik paměti.

Běžný tok

Na obrázku uvádíme haldu a zásobník aplikace.

Uživatel spustil aplikaci, otevřel ThreadActivity a čekal na obrazovce až do dokončení úlohy stahování. Uživatel čekal 20 sekund. Proč právě 20 vteřin? Protože to je doba, kterou vlákno potřebuje k dokončení úlohy.

Úloha běží

Úloha běží na pozadí. Uživatel čekal 20 sekund na dokončení úlohy stahování. Po dokončení úlohy zásobník uvolní blok metody run().

Není žádný odkaz, který by držel úlohu DownloadTask. GC považoval objekt DownladTask za nepoužívaný objekt, a proto jej v dalším cyklu GC vymaže z paměti haldy.

GC vymaže nepoužívané objekty z haldy. Nyní, když uživatel ukončí aktivitu. Hlavní metoda bude uvolněna ze zásobníku a v dalším cyklu GC vymaže z paměti haldy metodu ThreadActivity.

Perfektní!

.