Condividi tramite


Garbage Collection

Xamarin.Android usa il Garbage Collector di generazione semplice di Mono. Si tratta di un Garbage Collector mark-and-sweep con due generazioni e uno spazio oggetti di grandi dimensioni, con due tipi di raccolte:

  • Raccolte secondarie (raccoglie l'heap gen0)
  • Raccolte principali (raccoglie gli heap dello spazio oggetti di grandi dimensioni e Gen1).

Nota

In assenza di una raccolta esplicita tramite GC. Le raccolte Collect() sono su richiesta, in base alle allocazioni dell'heap. Non si tratta di un sistema di conteggio dei riferimenti. Gli oggetti non verranno raccolti non appena non sono presenti riferimenti in sospeso o quando un ambito è terminato. Il processo GC verrà eseguito quando l'heap secondario ha esaurito la memoria per le nuove allocazioni. Se non sono presenti allocazioni, non verrà eseguita.

Le raccolte secondarie sono economiche e frequenti e vengono usate per raccogliere oggetti allocati e non aggiornati di recente. Le raccolte secondarie vengono eseguite dopo pochi MB di oggetti allocati. Le raccolte secondarie possono essere eseguite manualmente chiamando GC. Raccogli (0)

Le raccolte principali sono costose e meno frequenti e vengono usate per recuperare tutti gli oggetti non recapitabili. Le raccolte principali vengono eseguite una volta esaurita la memoria per le dimensioni correnti dell'heap (prima di ridimensionare l'heap). Le raccolte principali possono essere eseguite manualmente chiamando GC. Raccogli () o chiamando GC. Raccogliere (int) con l'argomento GC. MaxGeneration.

Raccolte di oggetti tra macchine virtuali

Esistono tre categorie di tipi di oggetto.

  • Oggetti gestiti: tipi che non ereditano da Java.Lang.Object , ad esempio System.String. Questi vengono raccolti normalmente dal GC.

  • Oggetti Java: tipi Java presenti all'interno della macchina virtuale di runtime Android, ma non esposti alla macchina virtuale Mono. Questi sono noiosi, e non verranno discussi ulteriormente. Questi vengono raccolti normalmente dalla macchina virtuale di runtime Android.

  • Oggetti peer: tipi che implementano IJavaObject , ad esempio tutte le sottoclassi Java.Lang.Object e Java.Lang.Throwable . Le istanze di questi tipi hanno due "metà" un peer gestito e un peer nativo. Il peer gestito è un'istanza della classe C#. Il peer nativo è un'istanza di una classe Java all'interno della macchina virtuale di runtime Android e la proprietà IJavaObject.Handle C# contiene un riferimento globale JNI al peer nativo.

Esistono due tipi di peer nativi:

  • Peer del framework: tipi Java "Normali" che non conoscono nulla di Xamarin.Android, ad esempio android.content.Context.

  • Peer utente: wrapper chiamabili Android generati in fase di compilazione per ogni sottoclasse Java.Lang.Object presente all'interno dell'applicazione.

Poiché esistono due macchine virtuali all'interno di un processo Xamarin.Android, esistono due tipi di Garbage Collection:

  • Raccolte di runtime Android
  • Raccolte Mono

Le raccolte di runtime Android funzionano normalmente, ma con un'avvertenza: un riferimento globale JNI viene considerato come una radice GC. Di conseguenza, se è presente un riferimento globale JNI che contiene un oggetto vm di runtime Android, l'oggetto non può essere raccolto, anche se è altrimenti idoneo per la raccolta.

Le raccolte Mono sono la posizione in cui si verifica il divertimento. Gli oggetti gestiti vengono raccolti normalmente. Gli oggetti peer vengono raccolti eseguendo il processo seguente:

  1. Tutti gli oggetti Peer idonei per la raccolta Mono hanno il riferimento globale JNI sostituito con un riferimento globale debole JNI.

  2. Viene richiamato un GC della macchina virtuale di runtime Android. È possibile raccogliere qualsiasi istanza peer nativa.

  3. Vengono controllati i riferimenti globali deboli JNI creati in (1). Se il riferimento debole è stato raccolto, viene raccolto l'oggetto Peer. Se il riferimento debole non è stato raccolto, il riferimento debole viene sostituito con un riferimento globale JNI e l'oggetto Peer non viene raccolto. Nota: nell'API 14+, questo significa che il valore restituito da IJavaObject.Handle può cambiare dopo un GC.

Il risultato finale di tutto questo è che un'istanza di un oggetto Peer sarà attiva finché viene fatto riferimento da codice gestito (ad esempio archiviato in una static variabile) o a cui fa riferimento il codice Java. Inoltre, la durata dei peer nativi verrà estesa al di là di ciò che altrimenti sarebbe in tempo reale, perché il peer nativo non sarà raccogliebile fino a quando il peer nativo e il peer gestito non saranno raccoglibili.

Cicli di oggetti

Gli oggetti peer sono presenti logicamente sia all'interno del runtime Android che della macchina virtuale Mono. Ad esempio, un'istanza peer gestita Android.App.Activity avrà un'istanza Java peer del framework android.app.Activity corrispondente. Tutti gli oggetti che ereditano da Java.Lang.Object possono avere rappresentazioni all'interno di entrambe le macchine virtuali.

Tutti gli oggetti con rappresentazione in entrambe le macchine virtuali avranno durate estese rispetto agli oggetti presenti solo all'interno di una singola macchina virtuale , ad esempio .System.Collections.Generic.List<int> Chiamata di GC. Collect non raccoglie necessariamente questi oggetti, perché il GC di Xamarin.Android deve assicurarsi che l'oggetto non faccia riferimento a una macchina virtuale prima di raccoglierla.

Per ridurre la durata degli oggetti, è necessario richiamare Java.Lang.Object.Dispose(). In questo modo la connessione all'oggetto tra le due macchine virtuali verrà "sever" manualmente liberando il riferimento globale, consentendo così di raccogliere gli oggetti più velocemente.

Raccolte automatiche

A partire dalla versione 4.1.0, Xamarin.Android esegue automaticamente un GC completo quando viene superata una soglia gref. Questa soglia è il 90% dei grefs massimi noti per la piattaforma: 1800 grefs nell'emulatore (2000 max) e 46800 grefs sull'hardware (massimo 52000). Nota: Xamarin.Android conta solo i grefs creati da Android.Runtime.JNIEnv e non conoscerà altri grefs creati nel processo. Questo è solo un euristico.

Quando viene eseguita una raccolta automatica, verrà stampato un messaggio simile al seguente nel log di debug:

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

L'occorrenza di questa situazione non è deterministica e può verificarsi in tempi inopportuni (ad esempio, al centro del rendering grafico). Se viene visualizzato questo messaggio, è possibile eseguire una raccolta esplicita altrove oppure provare a ridurre la durata degli oggetti peer.

Opzioni bridge GC

Xamarin.Android offre una gestione trasparente della memoria con Android e il runtime Android. Viene implementato come estensione al Garbage Collector mono denominato GC Bridge.

Il bridge GC funziona durante un'operazione di Garbage Collection mono e individua quali oggetti peer hanno bisogno della loro "attività" verificata con l'heap di runtime Android. Il bridge GC fa questa determinazione eseguendo i passaggi seguenti (in ordine):

  1. Indurre il grafo di riferimento mono di oggetti peer non raggiungibili negli oggetti Java che rappresentano.

  2. Eseguire un GC Java.

  3. Verificare quali oggetti sono realmente morti.

Questo processo complesso è ciò che consente alle sottoclassi di fare Java.Lang.Object liberamente riferimento a qualsiasi oggetto; rimuove tutte le restrizioni su cui gli oggetti Java possono essere associati a C#. A causa di questa complessità, il processo bridge può essere molto costoso e può causare pause evidenti in un'applicazione. Se l'applicazione riscontra pause significative, è consigliabile esaminare una delle tre implementazioni di Bridge GC seguenti:

  • Tarjan - Una progettazione completamente nuova del bridge GC basata sull'algoritmo di Robert Tarjan e sulla propagazione dei riferimenti indietro. Offre prestazioni ottimali per i carichi di lavoro simulati, ma ha anche la quota più ampia di codice sperimentale.

  • Nuovo : una revisione importante del codice originale, correggendo due istanze del comportamento quadratico ma mantenendo l'algoritmo principale (basato sull'algoritmo di Kosaraju per trovare componenti fortemente connessi).

  • Old - L'implementazione originale (considerata la più stabile delle tre). Si tratta del bridge che un'applicazione deve usare se le GC_BRIDGE pause sono accettabili.

L'unico modo per capire quale GC Bridge funziona meglio è sperimentare in un'applicazione e analizzare l'output. Esistono due modi per raccogliere i dati per il benchmarking:

  • Abilita registrazione : abilitare la registrazione (come descritto nella sezione Configurazione ) per ogni opzione GC Bridge, quindi acquisire e confrontare gli output del log da ogni impostazione. Esaminare i GC messaggi per ogni opzione, in particolare i GC_BRIDGE messaggi. Le pause fino a 150 ms per le applicazioni non interattive sono tollerabili, ma le pause superiori a 60 ms per applicazioni molto interattive (ad esempio i giochi) rappresentano un problema.

  • Abilita contabilità bridge: la contabilità bridge visualizzerà il costo medio degli oggetti a cui punta ogni oggetto coinvolto nel processo bridge. L'ordinamento di queste informazioni in base alle dimensioni fornirà suggerimenti su ciò che contiene la quantità maggiore di oggetti aggiuntivi.

L'impostazione predefinita è Tarjan. Se si trova una regressione, potrebbe essere necessario impostare questa opzione su Vecchio. Inoltre, è possibile scegliere di usare l'opzione Old più stabile se Tarjan non produce un miglioramento delle prestazioni.

Per specificare quale GC_BRIDGE opzione deve essere usata da un'applicazione, passare bridge-implementation=oldbridge-implementation=new o bridge-implementation=tarjan alla variabile di MONO_GC_PARAMS ambiente. A tale scopo, aggiungere un nuovo file al progetto con un'azione Di compilazione di AndroidEnvironment. Ad esempio:

MONO_GC_PARAMS=bridge-implementation=tarjan

Per altre informazioni, vedere Configurazione.

Aiutare il GC

Esistono diversi modi per aiutare il GC a ridurre l'utilizzo della memoria e i tempi di raccolta.

Eliminazione di istanze peer

Il GC ha una visualizzazione incompleta del processo e potrebbe non essere eseguita quando la memoria è insufficiente perché il GC non sa che la memoria è insufficiente.

Ad esempio, un'istanza di un tipo Java.Lang.Object o un tipo derivato è di almeno 20 byte di dimensioni (soggette a modifiche senza preavviso e così via). I wrapper chiamabili gestiti non aggiungono membri di istanza aggiuntivi, quindi quando si dispone di un'istanza Android.Graphics.Bitmap che fa riferimento a un BLOB di memoria di 10 MB, il GC di Xamarin.Android non saprà che, il GC vedrà un oggetto a 20 byte e non sarà in grado di determinare che è collegato a oggetti allocati dal runtime Android che mantengono attivi 10 MB di memoria.

È spesso necessario aiutare il GC. Sfortunatamente, GC. AddMemoryPressure() e GC. RemoveMemoryPressure() non è supportato, quindi se si sa che è stato appena liberato un oggetto grafico allocato da Java di grandi dimensioni, potrebbe essere necessario chiamare manualmente GC. Collect() per richiedere a un GC di rilasciare la memoria sul lato Java oppure è possibile eliminare in modo esplicito le sottoclassi Java.Lang.Object , interrompendo il mapping tra il wrapper chiamabile gestito e l'istanza Java.

Nota

È necessario prestare molta attenzione quando si eliminano istanze di Java.Lang.Object sottoclasse.

Per ridurre al minimo la possibilità di danneggiamento della memoria, osservare le linee guida seguenti quando si chiama Dispose().

Condivisione tra più thread

Se l'istanza java o gestita può essere condivisa tra più thread, non deve mai essere Dispose()d. Ad esempio, Typeface.Create()può restituire un'istanza memorizzata nella cache. Se più thread forniscono gli stessi argomenti, otterranno la stessa istanza. Di conseguenza, Dispose()l'esecuzione dell'istanza Typeface da un thread può invalidare altri thread, il che può comportare ArgumentExceptionl'eliminazione dell'istanza da JNIEnv.CallVoidMethod() un altro thread.

Eliminazione di tipi Java associati

Se l'istanza è di un tipo Java associato, l'istanza può essere eliminata finché l'istanza non verrà riutilizzata dal codice gestito e l'istanza Java non può essere condivisa tra thread (vedere la discussione precedenteTypeface.Create()). (Rendere questa determinazione può essere difficile. La volta successiva che l'istanza Java immette codice gestito, verrà creato un nuovo wrapper.

Ciò è spesso utile quando si tratta di Drawables e altre istanze con un numero elevato di risorse:

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

Il valore precedente è sicuro perché il peer restituito da Drawable.CreateFromPath() farà riferimento a un peer framework, non a un peer utente. La Dispose() chiamata alla fine del using blocco interromperà la relazione tra le istanze Drawable e framework Drawable gestite, consentendo la raccolta dell'istanza Java non appena il runtime Android deve essere raccolto. Ciò non sarebbe sicuro se l'istanza peer fa riferimento a un peer utente. In questo caso si usano informazioni "esterne" per sapere che non Drawable può fare riferimento a un peer utente e quindi la Dispose() chiamata è sicura.

Eliminazione di altri tipi

Se l'istanza fa riferimento a un tipo che non è un'associazione di un tipo Java (ad esempio un oggetto personalizzatoActivity), NON chiamare Dispose() , a meno che non si sappia che nessun codice Java chiamerà metodi sottoposti a override in tale istanza. In caso contrario, i risultati sono i NotSupportedExceptionseguenti.

Ad esempio, se si dispone di un listener di clic personalizzato:

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

Non eliminare questa istanza, perché Java tenterà di richiamarvi i metodi in futuro:

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

Uso di controlli espliciti per evitare eccezioni

Se è stato implementato un metodo di overload Java.Lang.Object.Dispose , evitare di toccare oggetti che coinvolgono JNI. In questo modo è possibile creare una situazione di eliminazione doppia che consente al codice di tentare (in modo irreversibile) di accedere a un oggetto Java sottostante già sottoposto a Garbage Collection. In questo modo viene generata un'eccezione simile alla seguente:

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

Questa situazione si verifica spesso quando il primo eliminazione di un oggetto fa sì che un membro diventi Null e quindi un successivo tentativo di accesso su questo membro Null provoca la generazione di un'eccezione. In particolare, l'oggetto (che collega un'istanza gestita all'istanza Handle Java sottostante) viene invalidato al primo eliminazione, ma il codice gestito tenta comunque di accedere a questa istanza Java sottostante anche se non è più disponibile (vedere Managed Callable Wrapper per altre informazioni sul mapping tra istanze Java e istanze gestite).

Un buon modo per evitare questa eccezione consiste nel verificare in modo esplicito nel Dispose metodo che il mapping tra l'istanza gestita e l'istanza Java sottostante sia ancora valido, ovvero verificare se l'oggetto Handle è null (IntPtr.Zero) prima di accedere ai relativi membri. Ad esempio, il metodo seguente Dispose accede a un childViews oggetto :

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

Se un passaggio dispose iniziale causa childViews la presenza di un valore non valido Handle, l'accesso for al ciclo genererà un'eccezione ArgumentException. Aggiungendo un controllo Null esplicito Handle prima del primo childViews accesso, il metodo seguente Dispose impedisce l'esecuzione dell'eccezione:

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)
        {
            // ...
        }
    }
}

Ridurre le istanze a cui si fa riferimento

Ogni volta che un'istanza di un tipo o di una Java.Lang.Object sottoclasse viene analizzata durante il processo GC, è necessario analizzare anche l'intero grafico a oggetti a cui fa riferimento l'istanza. L'oggetto grafico è il set di istanze dell'oggetto a cui fa riferimento l'istanza radice, oltre a tutti gli elementi a cui fa riferimento l'istanza radice, in modo ricorsivo.

Si consideri la classe seguente:

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)));
    }
}

Quando BadActivity viene costruito, l'oggetto grafico conterrà 10004 istanze (1x BadActivity, 1x strings, 1x string[] mantenute da strings, 10000x istanze di stringa), che dovranno essere analizzate ogni volta che l'istanza BadActivity viene analizzata.

Ciò può avere effetti negativi sui tempi di raccolta, con conseguente aumento dei tempi di sospensione GC.

È possibile aiutare il GC riducendo le dimensioni dei grafici degli oggetti che sono rooted dalle istanze peer utente. Nell'esempio precedente, questa operazione può essere eseguita spostandosi BadActivity.strings in una classe separata che non eredita da 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)));
    }
}

Raccolte secondarie

Le raccolte secondarie possono essere eseguite manualmente chiamando GC. Collect(0). Le raccolte secondarie sono economiche (rispetto alle raccolte principali), ma hanno un costo fisso significativo, quindi non si vuole attivarle troppo spesso e dovrebbe avere un tempo di pausa di alcuni millisecondi.

Se l'applicazione ha un "ciclo di lavoro" in cui la stessa operazione viene eseguita più o più volte, può essere consigliabile eseguire manualmente una raccolta secondaria al termine del ciclo di lavoro. I cicli di servizio di esempio includono:

  • Ciclo di rendering di un singolo fotogramma di gioco.
  • L'intera interazione con una determinata finestra di dialogo dell'app (apertura, riempimento, chiusura)
  • Gruppo di richieste di rete per aggiornare/sincronizzare i dati dell'app.

Raccolte principali

Le raccolte principali possono essere eseguite manualmente chiamando GC. Collect() o GC.Collect(GC.MaxGeneration).

Devono essere eseguite raramente e possono avere un tempo di pausa di un secondo in un dispositivo in stile Android durante la raccolta di un heap da 512 MB.

Le raccolte principali devono essere richiamate manualmente, se mai:

  • Alla fine dei lunghi cicli di servizio e quando una pausa prolungata non presenta un problema all'utente.

  • All'interno di un metodo Android.App.Activity.OnLowMemory() sottoposto a override.

Diagnostica

Per tenere traccia della creazione e dell'eliminazione definitiva dei riferimenti globali, è possibile impostare la proprietà di sistema debug.mono.log in modo che contenga gref e/o gc.

Impostazione

Il Garbage Collector di Xamarin.Android può essere configurato impostando la MONO_GC_PARAMS variabile di ambiente. Le variabili di ambiente possono essere impostate con un'azione di compilazione di AndroidEnvironment.

La MONO_GC_PARAMS variabile di ambiente è un elenco delimitato da virgole dei parametri seguenti:

  • nursery-size = size : imposta le dimensioni del vivaio. La dimensione è specificata in byte e deve essere una potenza di due. I suffissi k e gm possono essere usati per specificare rispettivamente kilo-, mega- e gigabyte. Il vivaio è la prima generazione (di due). Un vivaio più grande di solito velocizza il programma, ma ovviamente userà più memoria. Dimensioni predefinite del vivaio 512 kb.

  • soft-heap-limit = size : il consumo massimo di memoria gestita di destinazione per l'app. Quando l'utilizzo della memoria è inferiore al valore specificato, il processo GC è ottimizzato per il tempo di esecuzione (meno raccolte). Al di sopra di questo limite, il GC è ottimizzato per l'utilizzo della memoria (più raccolte).

  • evacuation-threshold = soglia : imposta la soglia di evacuazione in percentuale. Il valore deve essere un numero intero compreso tra 0 e 100. Il valore predefinito è 66. Se la fase sweep della raccolta rileva che l'occupazione di un tipo di blocco heap specifico è inferiore a questa percentuale, eseguirà una raccolta di copia per quel tipo di blocco nella raccolta principale successiva, ripristinando così l'occupazione fino a quasi il 100%. Il valore 0 disattiva l'evacuazione.

  • bridge-implementation = implementazione bridge: verrà impostata l'opzione GC Bridge per risolvere i problemi di prestazioni GC. Esistono tre valori possibili: vecchio , nuovo , tarjan.

  • bridge-require-precise-merge: il ponte tarjan contiene un'ottimizzazione che può, in rari casi, causare la raccolta di un oggetto GC dopo che diventa spazzatura. L'inclusione di questa opzione disabilita l'ottimizzazione, rendendo i controller di dominio più prevedibili ma potenzialmente più lenti.

Ad esempio, per configurare GC per avere un limite di dimensioni heap pari a 128 MB, aggiungere un nuovo file al progetto con un'azione Di compilazione di AndroidEnvironment con il contenuto:

MONO_GC_PARAMS=soft-heap-limit=128m