Uvolňování paměti

Xamarin.Android používá systém Mono simple generational garbage collector. Jedná se o systém uvolňování paměti mark-and-uklidit se dvěma generacemi a velkým prostorem objektů se dvěma druhy kolekcí:

  • Menší kolekce (shromažďuje haldu Gen0)
  • Hlavní kolekce (shromažďuje gen1 a velké haldy prostoru objektů).

Poznámka:

Při absenci explicitní kolekce prostřednictvím uvolňování paměti. Kolekce Collect() jsou na vyžádání založené na přidělení haldy. Nejedná se o systém počítání odkazů. Objekty nebudou shromažďovány , jakmile nebudou žádné nevyhrazené odkazy nebo když dojde k ukončení oboru. GC se spustí, když menší halda vyčerpá paměť pro nové přidělení. Pokud nejsou k dispozici žádné přidělení, nespustí se.

Menší sbírky jsou levné a časté a používají se ke shromažďování nedávno přidělených a mrtvých objektů. Menší kolekce se provádějí po každém několika MB přidělených objektů. Menší kolekce lze provést ručně voláním uvolňování paměti. Collect (0)

Hlavní kolekce jsou nákladné a méně časté a používají se k uvolnění všech mrtvých objektů. Hlavní kolekce se provádějí po vyčerpání paměti pro aktuální velikost haldy (před změnou velikosti haldy). Hlavní kolekce mohou být provedeny ručně voláním uvolňování paměti. Shromážděte () nebo voláním GC. Shromážděte (int) s argumentem GC. MaxGeneration.

Kolekce objektů mezi virtuálními počítači

Existují tři kategorie typů objektů.

  • Spravované objekty: typy, které nedědí z Java.Lang.Object , například System.String. Tyto údaje obvykle shromažďuje GC.

  • Objekty Java: Typy Javy, které jsou přítomné na virtuálním počítači s Modulem Runtime pro Android, ale nejsou vystaveny mono virtuálnímu počítači. To jsou nudné a nebudou probírat dál. Tyto informace obvykle shromažďuje virtuální počítač s Modulem Android Runtime.

  • Partnerské objekty: typy, které implementují IJavaObject , například všechny podtřídy Java.Lang.Object a Java.Lang.Throwable . Instance těchto typů mají dva "poloviny" spravovaného partnerského vztahu a nativní partnerský vztah. Spravovaný partner je instance třídy C#. Nativní partnerský vztah je instance třídy Java v rámci virtuálního počítače s androidem runtime a vlastnost C# IJavaObject.Handle obsahuje globální odkaz JNI na nativní partnerský vztah.

Existují dva typy nativních partnerských vztahů:

  • Partnerské vztahy architektury : "Normální" typy Java, které neví nic o Xamarin.Android, například android.content.Context.

  • Partnerské vztahy uživatelů: Obálky s možností volání pro Android, které se generují v době sestavení pro každou podtřídu Java.Lang.Object, která se nachází v aplikaci.

Vzhledem k tomu, že v procesu Xamarin.Android existují dva virtuální počítače, existují dva typy uvolňování paměti:

  • Kolekce modulu runtime androidu
  • Mono kolekce

Kolekce runtime Androidu fungují normálně, ale s upozorněním: globální odkaz JNI se považuje za kořen uvolňování paměti. Pokud je tedy globální odkaz JNI držící se na objektu virtuálního počítače s Androidem Runtime, nelze objekt shromáždit, i když je jinak způsobilý pro kolekci.

Mono kolekce jsou místo, kde se zábava děje. Spravované objekty se shromažďují normálně. Partnerské objekty se shromažďují provedením následujícího procesu:

  1. Všechny partnerské objekty způsobilé pro kolekci Mono mají globální odkaz JNI nahrazený slabým globálním odkazem JNI.

  2. Vyvolá se GC virtuálního počítače s Androidem runtime. Je možné shromažďovat všechny nativní instance partnerského vztahu.

  3. Jsou kontrolovány slabé globální odkazy JNI vytvořené v (1). Pokud byl shromážděn slabý odkaz, je shromažďován objekt Peer. Pokud se slabý odkaz neshromažďoval , je slabý odkaz nahrazen globálním odkazem JNI a objekt Peer se neshromažďuje. Poznámka: V rozhraní API 14 nebo novějším to znamená, že hodnota vrácená z IJavaObject.Handle GC se může změnit.

Konečným výsledkem je, že instance peerového objektu bude aktivní, pokud na něj odkazuje spravovaný kód (např. uložený v static proměnné) nebo odkazovaný kódem Java. Životnost nativních partnerských uzlů se navíc rozšíří nad rámec toho, co by jinak žilo, protože nativní partnerský vztah nebude možné shromažďovat, dokud nebude možné shromažďovat nativní i spravovaný partnerský vztah.

Cykly objektů

Partnerské objekty se logicky nacházejí v modulu runtime Androidu i v mono virtuálním počítači. Například spravovaná instance partnerského uzlu Android.App.Activity bude mít odpovídající instanci v javě android.app.Activity Framework. Všechny objekty, které dědí z Java.Lang.Object , lze očekávat, že budou mít reprezentace v obou virtuálních počítačích.

Všechny objekty, které mají reprezentaci v obou virtuálních počítačích, budou mít životnosti rozšířené ve srovnání s objekty, které jsou přítomné pouze v rámci jednoho virtuálního počítače (například ).System.Collections.Generic.List<int> Volání GC. Shromažďování nemusí nutně shromažďovat tyto objekty, protože globální paměti Xamarin.Android musí před shromážděním zajistit, aby na objekt neodkazoval ani jeden virtuální počítač.

Chcete-li zkrátit životnost objektu, je třeba vyvolat Java.Lang.Object.Dispose(). Tím se ručně "odsadí" připojení k objektu mezi dvěma virtuálními počítači uvolněním globálního odkazu, čímž umožníte rychlejší shromažďování objektů.

Automatické kolekce

Od verze 4.1.0 Xamarin.Android při překročení prahové hodnoty gref automaticky provede úplné uvolňování paměti. Tato prahová hodnota je 90 % známých maximálních grefů pro platformu: 1800 grefs v emulátoru (2000 max) a 46800 grefs na hardwaru (maximálně 52000). Poznámka: Xamarin.Android počítá pouze grefs vytvořené v Android.Runtime.JNIEnv a nebude vědět o žádném jiném grefs vytvořeném v procesu. Tohle je jen heuristické.

Při provedení automatické kolekce se do protokolu ladění vytiskne zpráva podobná této:

I/monodroid-gc(PID): 46800 outstanding GREFs. Performing a full GC!

Výskyt tohoto problému není deterministický a může k tomu dojít v neportunech časech (např. uprostřed vykreslování grafiky). Pokud se tato zpráva zobrazí, můžete chtít explicitní kolekci provést jinde nebo se můžete pokusit zkrátit dobu života partnerských objektů.

Možnosti mostu GC

Xamarin.Android nabízí transparentní správu paměti s Androidem a modulem Android Runtime. Implementuje se jako rozšíření systému uvolňování paměti Mono označovaného jako most GC.

Most GC funguje během uvolňování paměti Mono a zjistí, které partnerské objekty potřebují svou "liveness" ověřenou haldou modulu runtime Androidu. Most GC toto rozhodnutí určí provedením následujících kroků (v pořadí):

  1. Indukujte mono referenční graf nedostupných partnerských objektů do objektů Javy, které představují.

  2. Proveďte GC v Javě.

  3. Ověřte, které objekty jsou opravdu mrtvé.

Tento složitý proces umožňuje Java.Lang.Object volně odkazovat na jakékoli objekty. Odebere všechna omezení, která mohou být objekty Java svázané s jazykem C#. Kvůli této složitosti může být proces mostu velmi nákladný a může způsobit výrazné pozastavení v aplikaci. Pokud u aplikace dochází k významným pozastavením, je vhodné prozkoumat jednu z následujících tří implementací mostu GC:

  • Tarjan - zcela nový design mostu GC založený na Robert Tarjanův algoritmus a zpětné šíření odkazu. Má nejlepší výkon v rámci našich simulovaných úloh, ale má také větší podíl experimentálního kódu.

  • Nový – hlavní oprava původního kódu, oprava dvou instancí kvadratického chování, ale udržování základního algoritmu (založeného na algoritmu Kosaraju pro nalezení silně propojených komponent).

  • Staré - Původní implementace (považována za nejstabilnější ze tří). Jedná se o most, který by aplikace měla použít, pokud GC_BRIDGE jsou pozastavení přijatelné.

Jediným způsobem, jak zjistit, který most GC funguje nejlépe, je experimentování v aplikaci a analýza výstupu. Existují dva způsoby, jak shromažďovat data pro srovnávací testy:

  • Povolit protokolování – Povolte protokolování (jak je popsáno v části Konfigurace ) pro každou možnost mostu GC a potom zachyťte a porovnejte výstupy protokolu z každého nastavení. GC Zkontrolujte zprávy pro každou možnost, zejména GC_BRIDGE zprávy. Pozastavení až 150 m pro neinteraktivní aplikace je možné tolerovat, ale u velmi interaktivních aplikací (například her) se pozastaví nad 60 ms.

  • Povolit účtování mostu – Účet mostu zobrazí průměrné náklady na objekty, na které odkazují jednotlivé objekty zapojené do procesu mostu. Seřazení těchto informací podle velikosti poskytne rady o tom, co drží největší množství dalších objektů.

Výchozí nastavení je Tarjan. Pokud najdete regresi, může být nutné nastavit tuto možnost na Starou. Také se můžete rozhodnout použít stabilnější Starou možnost, pokud Tarjan nevygeneruje zlepšení výkonu.

Chcete-li určit, kterou GC_BRIDGE možnost má aplikace použít, předat bridge-implementation=oldbridge-implementation=new nebo bridge-implementation=tarjan do MONO_GC_PARAMS proměnné prostředí. Toho dosáhnete tak, že do projektu přidáte nový soubor s akcí Sestavení .AndroidEnvironment Příklad:

MONO_GC_PARAMS=bridge-implementation=tarjan

Další informace najdete v tématu Konfigurace.

Pomoc GC

Existuje několik způsobů, jak GC pomoct snížit využití paměti a časy shromažďování.

Disposing of Peer instances

GC má neúplné zobrazení procesu a nemusí se spustit, pokud je paměť nízká, protože GC neví, že paměť je nízká.

Například instance typu Java.Lang.Object nebo odvozeného typu je velikost nejméně 20 bajtů (může se změnit bez upozornění atd.). Spravované obálky s možností volání nepřidají další členy instance, takže pokud máte instanci Android.Graphics.Bitmap , která odkazuje na 10MB objekt blob paměti, Xamarin.Android GC nebude vědět, že – GC uvidí 20 bajtový objekt a nebude moct zjistit, že je propojený s objekty přidělenými modulem runtime Androidu, které udržují 10 MB paměti naživu.

Často je nutné pomoct GC. Bohužel, GC. AddMemoryPressure() a GC. RemoveMemoryPressure() nejsou podporovány, takže pokud víte , že jste právě uvolnili velký graf objektů přidělený v Jazyce Java, možná budete muset ručně volat GC. Collect() k zobrazení výzvy uvolňování paměti na straně Javy, nebo můžete explicitně odstranit podtřídy Java.Lang.Object , které přeruší mapování mezi spravovaným volatelným obálkou a instancí Javy.

Poznámka:

Při odstraňování Java.Lang.Object instancí podtřídy musíte být velmi opatrní.

Chcete-li minimalizovat možnost poškození paměti, při volání Dispose()dodržujte následující pokyny .

Sdílení mezi více vlákny

Pokud může být spravovaná instance Java nebo spravovaná instance sdílena mezi více vlákny, neměla by být Dispose()nikdy d. Například Typeface.Create()může vrátit instanci uloženou v mezipaměti. Pokud více vláken poskytuje stejné argumenty, získá stejnou instanci. V důsledku toho Dispose()může inging Typeface instance z jednoho vlákna zneplatnit další vlákna, což může vést ArgumentExceptionk JNIEnv.CallVoidMethod() tomu, že instance byla uvolněna z jiného vlákna.

Disposing Bound Java Types

Pokud je instance vázaného typu Java, může být instance uvolněna , pokud instance nebude znovu použita ze spravovaného kódu a instance Javy se nedá sdílet mezi vlákny (viz předchozí Typeface.Create() diskuze). (Stanovení tohoto rozhodnutí může být obtížné.) Při příštím zadání spravovaného kódu instance Java se pro ni vytvoří nový obálka.

To je často užitečné, pokud jde o drawables a další instance náročné na prostředky:

using (var d = Drawable.CreateFromPath ("path/to/filename"))
    imageView.SetImageDrawable (d);

Výše uvedené hodnoty jsou bezpečné, protože partnerský vztah, který vrátí Drawable.CreateFromPath(), bude odkazovat na partnerský vztah architektury, nikoli na partnerský vztah uživatele. Volání Dispose() na konci using bloku přeruší vztah mezi spravovanými instancemi Drawable a Framework Drawable a umožní shromažďování instancí Javy, jakmile bude potřeba modul runtime Androidu. To by nebylo bezpečné, pokud instance peeru odkazuje na partnerský vztah uživatele. Tady používáme "externí" informace, abychom věděli , že Drawable nemůže odkazovat na partnerský vztah uživatele, a proto Dispose() je volání bezpečné.

Disposing Other Types

Pokud instance odkazuje na typ, který není vazbou typu Java (například vlastníActivity), nevolejteDispose(), pokud nevíte, že žádný kód Java nebude v dané instanci volat přepisované metody. Pokud to neuděláte, výsledkem NotSupportedExceptionbude s.

Pokud máte například vlastní naslouchací proces kliknutí:

partial class MyClickListener : Java.Lang.Object, View.IOnClickListener {
    // ...
}

Tuto instanci byste neměli odstraňovat, protože java se v budoucnu pokusí vyvolat metody:

// BAD CODE; DO NOT USE
Button b = FindViewById<Button> (Resource.Id.myButton);
using (var listener = new MyClickListener ())
    b.SetOnClickListener (listener);

Použití explicitních kontrol k zabránění výjimkám

Pokud jste implementovali metodu přetížení Java.Lang.Object.Dispose , vyhněte se dotykům objektů, které zahrnují JNI. Můžete tak vytvořit situaci s dvojitou uvolněním, která vašemu kódu umožní (závažně) se pokusit o přístup k podkladovému objektu Java, který už byl uvolněn z paměti. Tím dojde k výjimce podobné následující:

System.ArgumentException: 'jobject' must not be IntPtr.Zero.
Parameter name: jobject
at Android.Runtime.JNIEnv.CallVoidMethod

K této situaci často dochází, když první odstranění objektu způsobí, že člen se stane null a následný pokus o přístup na tomto členu null způsobí výjimku. Konkrétně objekt Handle (který propojí spravovanou instanci s jeho základní instancí Javy) je při první vyřazení neplatný, ale spravovaný kód se stále pokouší o přístup k této základní instanci Javy, i když už není k dispozici (další informace o mapování mezi instancemi Java a spravovanými instancemi najdete v tématu Spravované obálky s možností volání).

Dobrým způsobem, jak této výjimce zabránit, je explicitně ověřit ve vaší Dispose metodě, že mapování mezi spravovanou instancí a základní instancí Java je stále platné. To znamená, že před přístupem ke členům objektu zkontrolujte, jestli má objekt Handle hodnotu null (IntPtr.Zero). Například následující Dispose metoda přistupuje k objektu childViews :

class MyClass : Java.Lang.Object, ISomeInterface 
{
    protected override void Dispose (bool disposing)
    {
        base.Dispose (disposing);
        for (int i = 0; i < this.childViews.Count; ++i)
        {
            // ...
        }
    }
}

Pokud počáteční průchod dispose způsobí childViews , že je neplatný Handle, for přístup smyčky vyvolá výjimku ArgumentException. Přidáním explicitní Handle kontroly null před prvním childViews přístupem zabrání následující Dispose metoda výskytu výjimky:

class MyClass : Java.Lang.Object, ISomeInterface 
{
    protected override void Dispose (bool disposing)
    {
        base.Dispose (disposing);

        // Check for a null handle:
        if (this.childViews.Handle == IntPtr.Zero)
            return;

        for (int i = 0; i < this.childViews.Count; ++i)
        {
            // ...
        }
    }
}

Omezení odkazovaných instancí

Při každé kontrole instance Java.Lang.Object typu nebo podtřídy během GC musí být prohledán také celý graf objektu, na který instance odkazuje. Graf objektů je sada instancí objektů, na které odkazuje "kořenová instance", a vše , na co odkazuje kořenová instance, rekurzivně.

Vezměte v úvahu následující třídu:

class BadActivity : Activity {

    private List<string> strings;

    protected override void OnCreate (Bundle bundle)
    {
        base.OnCreate (bundle);

        strings.Value = new List<string> (
                Enumerable.Range (0, 10000)
                .Select(v => new string ('x', v % 1000)));
    }
}

Při BadActivity vytváření bude graf objektů obsahovat 1 0004 instancí (1x , 1x BadActivity, 1x, 1x stringsstring[] uchovávané strings, 10000x instance řetězců), z nichž všechny bude nutné kontrolovat při každé BadActivity kontrole instance.

To může mít negativní vliv na časy shromažďování, což vede ke zvýšení doby pozastavení uvolňování paměti.

GC můžete pomoct zmenšením velikosti grafů objektů, které jsou rootovány instancemi partnerských vztahů uživatelů. V předchozím příkladu to lze provést přesunutím BadActivity.strings do samostatné třídy, která nedědí z Java.Lang.Object:

class HiddenReference<T> {

    static Dictionary<int, T> table = new Dictionary<int, T> ();
    static int idgen = 0;

    int id;

    public HiddenReference ()
    {
        lock (table) {
            id = idgen ++;
        }
    }

    ~HiddenReference ()
    {
        lock (table) {
            table.Remove (id);
        }
    }

    public T Value {
        get { lock (table) { return table [id]; } }
        set { lock (table) { table [id] = value; } }
    }
}

class BetterActivity : Activity {

    HiddenReference<List<string>> strings = new HiddenReference<List<string>>();

    protected override void OnCreate (Bundle bundle)
    {
        base.OnCreate (bundle);

        strings.Value = new List<string> (
                Enumerable.Range (0, 10000)
                .Select(v => new string ('x', v % 1000)));
    }
}

Menší kolekce

Menší kolekce lze provést ručně voláním uvolňování paměti. Collect(0). Menší kolekce jsou levné (ve srovnání s hlavními kolekcemi), ale mají významné pevné náklady, takže je nechcete příliš často aktivovat a měli byste mít pauzu na několik milisekund.

Pokud má vaše aplikace "pracovní cyklus", ve kterém je stejná věc dokončena a překonaná, může být vhodné provést ručně menší sběr, jakmile pracovní cyklus skončí. Mezi příklady cyklů cel patří:

  • Cyklus vykreslování jednoho herního rámce.
  • Celá interakce s daným dialogem aplikace (otevření, vyplnění, zavření)
  • Skupina síťových požadavků na aktualizaci a synchronizaci dat aplikace.

Hlavní kolekce

Hlavní kolekce mohou být provedeny ručně voláním uvolňování paměti. Collect() nebo GC.Collect(GC.MaxGeneration).

Při shromažďování haldy o 512 MB by se měly provádět zřídka a mohou mít na zařízení se stylem Androidu čas pozastavení sekundy.

Hlavní kolekce by měly být vyvolány pouze ručně, pokud někdy:

Diagnostika

Pokud chcete sledovat, kdy se vytvoří a zničí globální odkazy, můžete nastavit vlastnost systému debug.mono.log tak, aby obsahovala gref a/nebo gc.

Konfigurace

Systém uvolňování paměti Xamarin.Android lze nakonfigurovat nastavením MONO_GC_PARAMS proměnné prostředí. Proměnné prostředí mohou být nastaveny akcí sestavení AndroidEnvironment.

MONO_GC_PARAMS Proměnná prostředí je čárkami oddělený seznam následujících parametrů:

  • nursery-size = velikost : Nastaví velikost mateřské školy. Velikost je určena v bajtech a musí být mocninou dvou. Přípony km a g lze použít k určení kilo-, mega- a gigabajtů v uvedeném pořadí. Mateřská škola je první generace (ze dvou). Větší dětská škola obvykle urychlí program, ale samozřejmě bude používat více paměti. Výchozí velikost dětské školy 512 kB.

  • soft-heap-limit = size : Cílová maximální spotřeba spravované paměti pro aplikaci. Pokud je využití paměti nižší než zadaná hodnota, je uvolňování paměti optimalizováno pro dobu provádění (méně kolekcí). Nad tímto limitem je uvolňování paměti optimalizované pro využití paměti (více kolekcí).

  • evacuation-threshold = prahová hodnota : Nastaví prahovou hodnotu pro evakuační hodnotu v procentech. Hodnota musí být celé číslo v rozsahu 0 až 100. Výchozí hodnota je 66. Pokud fáze úklidu kolekce zjistí, že obsazenost konkrétního typu bloku haldy je menší než toto procento, provede kopírování kolekce pro tento typ bloku v další hlavní kolekci, čímž obnoví obsazenost až do 100 procent. Hodnota 0 vypne odsuzení.

  • bridge-implementation = implementace mostu: Nastaví se možnost mostu GC, která pomůže vyřešit problémy s výkonem GC. Existují tři možné hodnoty: starý , nový , tarjan.

  • bridge-require-precise-merge: Most Tarjan obsahuje optimalizaci, která může ve výjimečných případech způsobit, že objekt bude shromážděn jeden GC poté, co se poprvé stane uvolňováním paměti. Zahrnutím této možnosti tuto optimalizaci zakážete, aby byly GCS předvídatelnější, ale potenciálně pomalejší.

Pokud chcete například nakonfigurovat GC tak, aby měl limit velikosti haldy 128 MB, přidejte do projektu nový soubor s akcíAndroidEnvironment Sestavení s obsahem:

MONO_GC_PARAMS=soft-heap-limit=128m