Partager via


Garbage collection

Xamarin.Android utilise le récupérateur de mémoire de génération simple de Mono. Il s’agit d’un garbage collector mark-and-sweep avec deux générations et un espace d’objet volumineux, avec deux types de collections :

  • Collections mineures (collecte le tas Gen0)
  • Collections principales (collecte les segments d’espace d’objets Gen1 et volumineux).

Remarque

En l’absence d’une collection explicite via GC. Les collections collect() sont à la demande, en fonction des allocations de tas. Il ne s’agit pas d’un système de comptage de références ; les objets ne seront pas collectés dès qu’il n’y a pas de références en attente ou lorsqu’une étendue a quitté. Le gc s’exécute lorsque le tas mineur a dépassé la mémoire pour les nouvelles allocations. S’il n’y a pas d’allocations, elle ne s’exécute pas.

Les collections mineures sont bon marché et fréquentes, et sont utilisées pour collecter des objets récemment alloués et morts. Les collections mineures sont effectuées après chaque Mo d’objets alloués. Les collections mineures peuvent être effectuées manuellement en appelant GC. Collecter (0)

Les collections principales sont coûteuses et moins fréquentes et sont utilisées pour récupérer tous les objets morts. Les collections principales sont effectuées une fois que la mémoire est épuisée pour la taille actuelle du tas (avant de redimensionner le tas). Les collections principales peuvent être effectuées manuellement en appelant GC. Collect () ou en appelant GC. Collect (int) avec l’argument GC. MaxGeneration.

Collections d’objets inter-machines virtuelles

Il existe trois catégories de types d’objets.

  • Objets managés : types qui n’héritent pas de Java.Lang.Object , par exemple System.String. Celles-ci sont collectées normalement par le GC.

  • Objets Java : types Java présents dans la machine virtuelle runtime Android, mais pas exposés à la machine virtuelle Mono. Ils sont ennuyeux, et ne seront pas discutés plus loin. Celles-ci sont collectées normalement par la machine virtuelle runtime Android.

  • Objets homologues : types qui implémentent IJavaObject, par exemple, toutes les sous-classes Java.Lang.Object et Java.Lang.Throwable. Les instances de ces types ont deux « moitiés » d’homologue managé et d’homologue natif. L’homologue managé est une instance de la classe C#. L’homologue natif est une instance d’une classe Java au sein de la machine virtuelle runtime Android, et la propriété C# IJavaObject.Handle contient une référence globale JNI à l’homologue natif.

Il existe deux types de pairs natifs :

  • Pairs d’infrastructure : types Java « Normal » qui ne connaissent rien de Xamarin.Android, par exemple android.content.Context.

  • Pairs utilisateur : Wrappers pouvant être appelé Android qui sont générés au moment de la génération pour chaque sous-classe Java.Lang.Object présente dans l’application.

Comme il existe deux machines virtuelles dans un processus Xamarin.Android, il existe deux types de garbage collections :

  • Collections de runtime Android
  • Collections Mono

Les collections de runtime Android fonctionnent normalement, mais avec une mise en garde : une référence globale JNI est traitée comme une racine GC. Par conséquent, s’il existe une référence globale JNI contenant sur un objet de machine virtuelle runtime Android, l’objet ne peut pas être collecté, même s’il est autrement éligible à la collection.

Les collections Mono sont là où le plaisir se produit. Les objets managés sont collectés normalement. Les objets homologues sont collectés en effectuant le processus suivant :

  1. Tous les objets homologues éligibles pour la collection Mono ont leur référence globale JNI remplacée par une référence globale faible JNI.

  2. Un gc de machine virtuelle runtime Android est appelé. Toute instance d’homologue native peut être collectée.

  3. Les références globales faibles JNI créées dans (1) sont case activée. Si la référence faible a été collectée, l’objet Peer est collecté. Si la référence faible n’a pas été collectée, la référence faible est remplacée par une référence globale JNI et l’objet Peer n’est pas collecté. Remarque : sur l’API 14+, cela signifie que la valeur retournée IJavaObject.Handle peut changer après un GC.

Le résultat final de tout cela est qu’une instance d’un objet Homologue vit tant qu’elle est référencée par du code managé (par exemple, stocké dans une static variable) ou référencée par du code Java. En outre, la durée de vie des pairs natifs sera étendue au-delà de ce qu’ils vivraient autrement, car l’homologue natif ne sera pas collectable tant que l’homologue natif et l’homologue managé ne seront pas collectés.

Cycles d’objets

Les objets homologues sont présents logiquement dans le runtime Android et les machines virtuelles Mono. Par exemple, une instance d’homologue managée Android.App.Activity aura une instance Java homologue android.app.Activity framework correspondante. Tous les objets qui héritent de Java.Lang.Object peuvent être censés avoir des représentations dans les deux machines virtuelles.

Tous les objets qui ont une représentation dans les deux machines virtuelles auront des durées de vie qui sont étendues par rapport aux objets qui sont présents uniquement dans une seule machine virtuelle (par exemple, a System.Collections.Generic.List<int>). Appel du GC. Collect ne collecte pas nécessairement ces objets, car le GC Xamarin.Android doit s’assurer que l’objet n’est pas référencé par l’une ou l’autre machine virtuelle avant de le collecter.

Pour raccourcir la durée de vie de l’objet, Java.Lang.Object.Dispose() doit être appelé. Cela permet de « couper » manuellement la connexion sur l’objet entre les deux machines virtuelles en libérant la référence globale, ce qui permet aux objets d’être collectés plus rapidement.

Regroupements automatiques

À compter de la version 4.1.0, Xamarin.Android effectue automatiquement un GC complet lorsqu’un seuil gref est franchi. Ce seuil est de 90 % des grefs maximum connus pour la plateforme : 1800 grefs sur l’émulateur (2000 max) et 46800 grefs sur le matériel (maximum 52000). Remarque : Xamarin.Android compte uniquement les grefs créés par Android.Runtime.JNIEnv et ne connaîtra pas les autres grefs créés dans le processus. C’est une heuristique uniquement.

Lorsqu’une collection automatique est effectuée, un message similaire à ce qui suit est imprimé dans le journal de débogage :

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

L’occurrence de ceci est non déterministe et peut se produire à des moments inopportunes (par exemple, au milieu du rendu graphique). Si vous voyez ce message, vous pouvez effectuer une collection explicite ailleurs ou essayer de réduire la durée de vie des objets homologues.

Options de pont GC

Xamarin.Android offre une gestion transparente de la mémoire avec Android et le runtime Android. Il est implémenté en tant qu’extension du garbage collector Mono appelé pont GC.

Le pont GC fonctionne pendant un garbage collection Mono et détermine quels objets homologues ont besoin de leur « liveness » vérifié avec le tas d’exécution Android. Le pont GC effectue cette détermination en effectuant les étapes suivantes (dans l’ordre) :

  1. Induire le graphique de référence mono d’objets homologues inaccessibles dans les objets Java qu’ils représentent.

  2. Effectuez un GC Java.

  3. Vérifiez quels objets sont vraiment morts.

Ce processus compliqué est ce qui permet aux sous-classes de Java.Lang.Object référencer librement tous les objets ; il supprime toutes les restrictions sur les objets Java pouvant être liés à C#. En raison de cette complexité, le processus de pont peut être très coûteux et peut entraîner des pauses notables dans une application. Si l’application rencontre des pauses significatives, il vaut la peine d’examiner l’une des trois implémentations de pont GC suivantes :

  • Tarjan - Une nouvelle conception du pont GC basée sur l’algorithme de Robert Tarjan et la propagation de référence descendante. Elle offre les meilleures performances sous nos charges de travail simulées, mais elle a également la plus grande part du code expérimental.

  • Nouveau - Une révision majeure du code d’origine, en corrigeant deux instances de comportement quadratique, mais en conservant l’algorithme de base (basé sur l’algorithme de Kosaraju pour rechercher des composants fortement connectés).

  • Ancien - Implémentation d’origine (considérée comme la plus stable des trois). Il s’agit du pont qu’une application doit utiliser si les GC_BRIDGE pauses sont acceptables.

La seule façon de déterminer quel pont GC fonctionne le mieux est d’expérimenter dans une application et d’analyser la sortie. Il existe deux façons de collecter les données pour l’évaluation :

  • Activer la journalisation : activez la journalisation (comme décrit dans la section Configuration ) pour chaque option de pont GC, puis capturez et comparez les sorties du journal de chaque paramètre. Inspectez les GC messages pour chaque option ; en particulier les GC_BRIDGE messages. Les pauses jusqu’à 150 ms pour les applications non interactives sont tolérables, mais les pauses supérieures à 60 ms pour les applications très interactives (comme les jeux) sont un problème.

  • Activer la comptabilité de pont : la comptabilité de pont affiche le coût moyen des objets pointés par chaque objet impliqué dans le processus de pont. Le tri de ces informations par taille fournit des indications sur ce qui contient la plus grande quantité d’objets supplémentaires.

Le paramètre par défaut est Tarjan. Si vous trouvez une régression, vous devrez peut-être définir cette option sur Old. En outre, vous pouvez choisir d’utiliser l’option Ancienne plus stable si Tarjan ne produit pas d’amélioration des performances.

Pour spécifier l’option GC_BRIDGE qu’une application doit utiliser, passer bridge-implementation=oldou bridge-implementation=newbridge-implementation=tarjan à la variable d’environnement MONO_GC_PARAMS . Pour ce faire, ajoutez un nouveau fichier à votre projet avec une action build de AndroidEnvironment. Par exemple :

MONO_GC_PARAMS=bridge-implementation=tarjan

Pour plus d’informations, consultez Configuration.

Aider le GC

Il existe plusieurs façons d’aider le GC à réduire l’utilisation de la mémoire et les heures de collecte.

Suppression d’instances d’homologue

Le GC a une vue incomplète du processus et peut ne pas s’exécuter lorsque la mémoire est faible, car le GC ne sait pas que la mémoire est faible.

Par exemple, une instance d’un type Java.Lang.Object ou d’un type dérivé est d’au moins 20 octets de taille (sous réserve de modification sans préavis, etc.). Les wrappers pouvant être gérés n’ajoutent pas de membres d’instance supplémentaires. Par conséquent, lorsque vous disposez d’une instance Android.Graphics.Bitmap qui fait référence à un objet blob de 10 Mo de mémoire, le GC de Xamarin.Android ne sait pas que : le GC voit un objet de 20 octets et ne peut pas déterminer qu’il est lié à des objets alloués par le runtime Android qui conservent 10 Mo de la mémoire active.

Il est fréquemment nécessaire d’aider le GC. Malheureusement, GC. AddMemoryPressure() et GC. RemoveMemoryPressure() n’est pas pris en charge. Par conséquent, si vous savez que vous venez de libérer un grand graphique d’objets alloué à Java, vous devrez peut-être appeler manuellement GC. Collect() pour inviter un GC à libérer la mémoire côté Java, ou vous pouvez supprimer explicitement les sous-classes Java.Lang.Object, en cassant le mappage entre le wrapper pouvant être appelé managé et l’instance Java.

Remarque

Vous devez être extrêmement prudent lors de la suppression d’instances de Java.Lang.Object sous-classe.

Pour réduire la possibilité d’altération de la mémoire, observez les instructions suivantes lors de l’appel Dispose().

Partage entre plusieurs threads

Si l’instance Java ou managée peut être partagée entre plusieurs threads, elle ne doit pas être Dispose()d, jamais. Par exemple, Typeface.Create() peut retourner une instance mise en cache. Si plusieurs threads fournissent les mêmes arguments, ils obtiennent la même instance. Par conséquent, Dispose()la mise en place de l’instance Typeface d’un thread peut invalider d’autres threads, ce qui peut entraîner ArgumentExceptionla suppression de JNIEnv.CallVoidMethod() l’instance (entre autres) car l’instance a été supprimée d’un autre thread.

Suppression des types Java liés

Si l’instance est d’un type Java lié, l’instance peut être supprimée tant que l’instance ne sera pas réutilisée à partir du code managé et que l’instance Java ne peut pas être partagée entre les threads (voir la discussion précédente Typeface.Create() ). (Rendre cette détermination peut être difficile.) La prochaine fois que l’instance Java entre dans le code managé, un nouveau wrapper sera créé pour celui-ci.

Cela est fréquemment utile lorsqu’il s’agit de Dessinables et d’autres instances gourmandes en ressources :

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

La valeur ci-dessus est sécurisée, car l’homologue retourné par Drawable.CreateFromPath() fait référence à un homologue Framework, et non à un homologue utilisateur. L’appel Dispose() à la fin du using bloc interrompt la relation entre les instances Drawable managées et Drawable de l’infrastructure, ce qui permet à l’instance Java d’être collectée dès que le runtime Android doit le faire. Cela ne serait pas sûr si l’instance d’homologue a fait référence à un homologue d’utilisateur ; ici, nous utilisons des informations « externes » pour savoir que le Drawable serveur ne peut pas faire référence à un homologue d’utilisateur, et par conséquent, l’appel Dispose() est sécurisé.

Suppression d’autres types

Si l’instance fait référence à un type qui n’est pas une liaison d’un type Java (tel qu’un type personnaliséActivity), N’appelez Dispose() PAS, sauf si vous savez qu’aucun code Java n’appelle les méthodes substituées sur cette instance. Le fait de ne pas le faire entraîne.NotSupportedException

Par exemple, si vous avez un écouteur de clic personnalisé :

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

Vous ne devez pas supprimer cette instance, car Java tentera d’appeler des méthodes sur celle-ci à l’avenir :

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

Utilisation de vérifications explicites pour éviter les exceptions

Si vous avez implémenté une méthode de surcharge Java.Lang.Object.Dispose , évitez de toucher des objets qui impliquent JNI. Cela peut créer une situation de double suppression qui permet à votre code d’accéder (irrécupérablement) à un objet Java sous-jacent qui a déjà été collecté par le garbage-collect. Cela produit une exception similaire à ce qui suit :

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

Cette situation se produit souvent lorsque la première suppression d’un objet entraîne la levée d’un membre comme null, puis une tentative d’accès ultérieure sur ce membre Null entraîne la levée d’une exception. Plus précisément, l’objet Handle (qui lie une instance managée à son instance Java sous-jacente) est invalidé lors de la première suppression, mais le code managé tente toujours d’accéder à cette instance Java sous-jacente, même si elle n’est plus disponible (voir Wrappers pouvant être appelé managé pour plus d’informations sur le mappage entre les instances Java et les instances managées).

Une bonne façon d’empêcher cette exception consiste à vérifier explicitement dans votre Dispose méthode que le mappage entre l’instance managée et l’instance Java sous-jacente est toujours valide ; autrement dit, case activée pour voir si l’objet est null (IntPtr.Zero) avant d’accéder Handle à ses membres. Par exemple, la méthode suivante Dispose accède à un childViews objet :

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

Si une passe de suppression initiale provoque childViews un non valide Handle, l’accès à la for boucle lève un ArgumentException. En ajoutant une case activée null explicite Handle avant le premier childViews accès, la méthode suivante Dispose empêche l’exception de se produire :

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

Réduire les instances référencées

Chaque fois qu’une instance d’un type ou d’une Java.Lang.Object sous-classe est analysée pendant le GC, le graphique d’objet entier auquel l’instance fait référence doit également être analysé. Le graphique d’objets est l’ensemble d’instances d’objet auxquelles l'« instance racine » fait référence, ainsi que tout ce qui est référencé par l’instance racine, de manière récursive.

Considérez la classe suivante :

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

Quand BadActivity il est construit, le graphe d’objets contient 1 0004 instances (1x, 1x stringsBadActivity, 1x string[] détenus par strings, 1 0000 instances de chaîne), qui doivent toutes être analysées chaque fois que l’instance BadActivity est analysée.

Cela peut avoir des répercussions néfastes sur vos heures de collecte, ce qui entraîne une augmentation des temps de pause du GC.

Vous pouvez aider le GC en réduisant la taille des graphiques d’objets qui sont rootés par les instances d’homologue utilisateur. Dans l’exemple ci-dessus, vous pouvez effectuer cette opération en déplaçant BadActivity.strings vers une classe distincte qui n’hérite pas de 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)));
    }
}

Collections mineures

Les collections mineures peuvent être effectuées manuellement en appelant GC. Collect(0). Les collections mineures sont bon marché (par rapport aux grands regroupements), mais ont un coût fixe important, donc vous ne voulez pas les déclencher trop souvent et doivent avoir un temps de pause de quelques millisecondes.

Si votre application a un « cycle de service » dans lequel la même chose est effectuée et terminée, il peut être conseillé d’effectuer manuellement une collecte mineure une fois le cycle de service terminé. Voici quelques exemples de cycles de service :

  • Cycle de rendu d’une trame de jeu unique.
  • Toute l’interaction avec une boîte de dialogue d’application donnée (ouverture, remplissage, fermeture)
  • Groupe de demandes réseau pour actualiser/synchroniser les données d’application.

Collections majeures

Les collections principales peuvent être effectuées manuellement en appelant GC. Collect() ou GC.Collect(GC.MaxGeneration).

Elles doivent être effectuées rarement et peuvent avoir un temps de pause d’une seconde sur un appareil de style Android lors de la collecte d’un tas 512 Mo.

Les collections principales ne doivent être appelées manuellement que si jamais :

  • À la fin des cycles de travail longs et quand une longue pause ne présente pas de problème à l’utilisateur.

  • Dans une méthode Android.App.Activity.OnLowMemory() substituée.

Diagnostics

Pour effectuer le suivi de la création et de la destruction de références globales, vous pouvez définir la propriété système debug.mono.log pour contenir gref et/ou gc.

Configuration

Le garbage collector Xamarin.Android peut être configuré en définissant la variable d’environnement MONO_GC_PARAMS . Les variables d’environnement peuvent être définies avec une action build d’AndroidEnvironment.

La MONO_GC_PARAMS variable d’environnement est une liste séparée par des virgules des paramètres suivants :

  • nursery-size = taille : définit la taille de la pépinière. La taille est spécifiée en octets et doit être une puissance de deux. Les suffixes k et mg peuvent être utilisés pour spécifier respectivement kilo-, méga et gigaoctets. La pépinière est la première génération (de deux). Une plus grande pépinière accélérera généralement le programme, mais utilisera évidemment plus de mémoire. Taille de la pépinière par défaut de 512 Ko.

  • soft-heap-limit = taille : consommation maximale de mémoire managée cible pour l’application. Lorsque l’utilisation de la mémoire est inférieure à la valeur spécifiée, le GC est optimisé pour le temps d’exécution (moins de collections). Au-dessus de cette limite, le GC est optimisé pour l’utilisation de la mémoire (plus de regroupements).

  • evacuation-threshold = seuil : définit le seuil d’évacuation en pourcentage. La valeur doit être un entier compris entre 0 et 100. La valeur par défaut est 66. Si la phase de balayage de la collection constate que l’occupation d’un type de bloc de tas spécifique est inférieure à ce pourcentage, elle effectue une collection de copie pour ce type de bloc dans la collection principale suivante, ce qui restaure l’occupation à près de 100 %. La valeur 0 désactive l’évacuation.

  • bridge-implementation = implémentation de pont : cette option définit l’option Pont GC pour aider à résoudre les problèmes de performances du GC. Il existe trois valeurs possibles : ancienne , nouvelle , tarjan.

  • bridge-require-precise-merge: Le pont Tarjan contient une optimisation qui peut, à de rares occasions, entraîner la collecte d’un objet d’un GC après qu’il devient garbage. L’inclusion de cette option désactive cette optimisation, ce qui rend les contrôleurs de groupe plus prévisibles mais potentiellement plus lents.

Par exemple, pour configurer le gc pour qu’il dispose d’une limite de taille de tas de 128 Mo, ajoutez un nouveau fichier à votre projet avec une action build du AndroidEnvironment contenu :

MONO_GC_PARAMS=soft-heap-limit=128m