Recolección de elementos no utilizados

Xamarin.Android usa el recolector de elementos no utilizados de generación simple de Mono. Se trata de un recolector de elementos no utilizados de marcado y barrido con dos generaciones y un espacio de objetos grande, con dos tipos de colecciones:

  • Colecciones secundarias (recopila el montón gen0)
  • Colecciones principales (recopila montones de espacio de objetos gen1 y grande).

Nota:

En ausencia de una colección explícita a través de GC. Las recopilaciones collect() están a petición, en función de las asignaciones del montón. Este no es un sistema de recuento de referencias; los objetos no se recopilarán tan pronto como no haya referencias pendientes o cuando se haya salido de un ámbito. El GC se ejecutará cuando el montón secundario se haya quedado sin memoria para las nuevas asignaciones. Si no hay asignaciones, no se ejecutará.

Las colecciones secundarias son baratas y frecuentes, y se usan para recopilar objetos asignados y fallidos recientemente. Las colecciones secundarias se realizan después de cada pocos MB de objetos asignados. Las colecciones secundarias se pueden realizar manualmente mediante una llamada a GC. Recopilar (0)

Las colecciones principales son costosas y menos frecuentes, y se usan para reclamar todos los objetos fallidos. Las colecciones principales se realizan una vez que se agota la memoria para el tamaño del montón actual (antes de cambiar el tamaño del montón). Las colecciones principales se pueden realizar manualmente mediante una llamada a GC. Recopile () o llame a GC. Recopile (int) con el argumento GC. MaxGeneration.

Colecciones de objetos entre máquinas virtuales

Hay tres categorías de tipos de objeto.

  • Objetos administrados: tipos que no heredan de Java.Lang.Object , por ejemplo , System.String. Normalmente, el GC recopila estos datos.

  • Objetos java: tipos de Java que están presentes en la máquina virtual en tiempo de ejecución de Android, pero que no se exponen a la máquina virtual mono. Estos son aburridos y no se analizarán más. Normalmente, estas se recopilan mediante la máquina virtual en tiempo de ejecución de Android.

  • Objetos del mismo nivel: tipos que implementan IJavaObject, por ejemplo, todas las subclases Java.Lang.Object y Java.Lang.Throwable. Las instancias de estos tipos tienen dos "mitades" de un mismo nivel administrado y un par nativo. El mismo nivel administrado es una instancia de la clase de C#. El mismo nivel nativo es una instancia de una clase Java dentro de la máquina virtual en tiempo de ejecución de Android y la propiedad IJavaObject.Handle de C# contiene una referencia global de JNI al mismo nivel nativo.

Hay dos tipos de pares nativos:

  • Elementos del mismo nivel del marco: tipos de Java "Normal" que no conocen nada de Xamarin.Android, por ejemplo , android.content.Context.

  • Elementos del mismo nivel de usuario: contenedores invocables de Android que se generan en tiempo de compilación para cada subclase Java.Lang.Object presente en la aplicación.

Como hay dos máquinas virtuales dentro de un proceso de Xamarin.Android, hay dos tipos de recolecciones de elementos no utilizados:

  • Colecciones en tiempo de ejecución de Android
  • Colecciones mono

Las colecciones en tiempo de ejecución de Android funcionan normalmente, pero con una advertencia: una referencia global de JNI se trata como una raíz de GC. Por lo tanto, si hay una referencia global de JNI que contiene un objeto de máquina virtual en tiempo de ejecución de Android, el objeto no se puede recopilar, aunque sea apto para la recopilación.

Las colecciones Mono son donde sucede la diversión. Los objetos administrados se recopilan normalmente. Los objetos del mismo nivel se recopilan realizando el siguiente proceso:

  1. Todos los objetos Peer aptos para la colección Mono tienen su referencia global de JNI reemplazada por una referencia global débil de JNI.

  2. Se invoca una gc de máquina virtual en tiempo de ejecución de Android. Se puede recopilar cualquier instancia del mismo nivel nativa.

  3. Se comprueban las referencias globales débiles de JNI creadas en (1). Si se ha recopilado la referencia débil, se recopila el objeto Peer. Si no se ha recopilado la referencia débil, la referencia débil se reemplaza por una referencia global de JNI y el objeto Peer no se recopila. Nota: en la API 14+, esto significa que el valor devuelto de puede cambiar después de IJavaObject.Handle un GC.

El resultado final de todo esto es que una instancia de un objeto Peer residirá siempre que se haga referencia a ella mediante código administrado (por ejemplo, almacenado en una static variable) o al que hace referencia el código Java. Además, la duración de los elementos del mismo nivel nativo se extenderá más allá de lo que de otro modo vivirían, ya que el elemento del mismo nivel nativo no se podrá recopilar hasta que se puedan recopilar los pares nativos y administrados.

Ciclos de objetos

Los objetos del mismo nivel están presentes lógicamente en el entorno de ejecución de Android y en la máquina virtual Mono. Por ejemplo, una instancia del mismo nivel administrada android.App.Activity tendrá una instancia de Java del mismo nivel de android.app.Activity framework correspondiente. Se puede esperar que todos los objetos que hereden de Java.Lang.Object tengan representaciones dentro de ambas máquinas virtuales.

Todos los objetos que tienen representación en ambas máquinas virtuales tendrán una duración que se extiende en comparación con los objetos que solo están presentes dentro de una sola máquina virtual (por ejemplo, ).System.Collections.Generic.List<int> Llamada a GC. Collect no recopilará necesariamente estos objetos, ya que el GC de Xamarin.Android debe asegurarse de que ninguna de las máquinas virtuales haga referencia al objeto antes de recopilarlo.

Para acortar la duración del objeto, se debe invocar Java.Lang.Object.Dispose(). Esto "severá" manualmente la conexión en el objeto entre las dos máquinas virtuales liberando la referencia global, lo que permite que los objetos se recopilen más rápido.

Recopilaciones automáticas

A partir de release 4.1.0, Xamarin.Android realiza automáticamente un GC completo cuando se cruza un umbral de gref. Este umbral es el 90 % del grefs máximo conocido para la plataforma: 1800 grefs en el emulador (2000 max) y 46800 grefs en hardware (máximo 52000). Nota: Xamarin.Android solo cuenta los grefs creados por Android.Runtime.JNIEnv y no conocerán ningún otro grefs creado en el proceso. Esto es sólo heurístico.

Cuando se realiza una recopilación automática, se imprimirá un mensaje similar al siguiente en el registro de depuración:

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

La aparición de esto no es determinista y puede ocurrir en momentos inoportunos (por ejemplo, en medio de la representación de gráficos). Si ve este mensaje, es posible que desee realizar una colección explícita en otro lugar o puede intentar reducir la duración de los objetos del mismo nivel.

Opciones del puente gc

Xamarin.Android ofrece administración de memoria transparente con Android y el entorno de ejecución de Android. Se implementa como una extensión para el recolector de elementos no utilizados Mono denominado puente gc.

El puente gc funciona durante una recolección de elementos no utilizados mono y determina qué objetos del mismo nivel necesitan su "vida" comprobado con el montón de tiempo de ejecución de Android. El puente GC realiza esta determinación realizando los pasos siguientes (en orden):

  1. Induce el gráfico de referencia mono de objetos del mismo nivel inaccesibles a los objetos java que representan.

  2. Realice una gc de Java.

  3. Compruebe qué objetos están realmente inactivos.

Este proceso complicado es lo que permite a las subclases de Java.Lang.Object hacer referencia libremente a cualquier objeto; quita las restricciones en las que se pueden enlazar objetos Java a C#. Debido a esta complejidad, el proceso de puente puede ser muy costoso y puede provocar pausas notables en una aplicación. Si la aplicación está experimentando pausas significativas, merece la pena investigar una de las tres implementaciones siguientes de GC Bridge:

  • Tarjan : un diseño completamente nuevo del puente GC basado en el algoritmo de Robert Tarjan y la propagación de referencia hacia atrás. Tiene el mejor rendimiento en nuestras cargas de trabajo simuladas, pero también tiene el mayor recurso de código experimental.

  • Nuevo : una revisión importante del código original, que corrige dos instancias de comportamiento cuadrático, pero manteniendo el algoritmo principal (basado en el algoritmo de Kosaraju para buscar componentes fuertemente conectados).

  • Antiguo : la implementación original (considerada la más estable de las tres). Este es el puente que una aplicación debe usar si las GC_BRIDGE pausas son aceptables.

La única manera de averiguar qué puente gc funciona mejor es experimentar en una aplicación y analizar la salida. Hay dos maneras de recopilar los datos para realizar pruebas comparativas:

  • Habilitar el registro : habilite el registro (como se describe en la sección Configuración ) para cada opción de puente de GC y, a continuación, capture y compare las salidas de registro de cada configuración. Inspeccione los GC mensajes de cada opción; en particular, los GC_BRIDGE mensajes. Las pausas de hasta 150 ms para aplicaciones no interactivas son tolerables, pero las pausas superiores a 60 ms para aplicaciones muy interactivas (como juegos) son un problema.

  • Habilitar la contabilidad de puentes: la contabilidad del puente mostrará el costo medio de los objetos apuntados por cada objeto implicado en el proceso del puente. La ordenación de esta información por tamaño proporcionará sugerencias sobre lo que contiene la mayor cantidad de objetos adicionales.

El valor predeterminado es Tarjan. Si encuentra una regresión, puede que sea necesario establecer esta opción en Antiguo. Además, puede optar por usar la opción Old más estable si Tarjan no produce una mejora en el rendimiento.

Para especificar qué GC_BRIDGE opción debe usar una aplicación, pasar bridge-implementation=oldbridge-implementation=new o bridge-implementation=tarjan a la variable de MONO_GC_PARAMS entorno. Esto se logra agregando un nuevo archivo al proyecto con una acción De compilación de AndroidEnvironment. Por ejemplo:

MONO_GC_PARAMS=bridge-implementation=tarjan

Para obtener más información, vea Configuración.

Ayudar a la GC

Hay varias maneras de ayudar al GC a reducir el uso de memoria y los tiempos de recopilación.

Eliminación de instancias del mismo nivel

El GC tiene una vista incompleta del proceso y es posible que no se ejecute cuando la memoria es baja porque el GC no sabe que la memoria es baja.

Por ejemplo, una instancia de un tipo Java.Lang.Object o un tipo derivado tiene al menos 20 bytes de tamaño (sujeto a cambios sin previo aviso, etc.). Los contenedores invocables administrados no agregan miembros de instancia adicionales, por lo que cuando tenga una instancia de Android.Graphics.Bitmap que haga referencia a un blob de memoria de 10 MB, el GC de Xamarin.Android no sabrá que : el GC verá un objeto de 20 bytes y no podrá determinar que está vinculado a objetos asignados en tiempo de ejecución de Android que mantienen activa 10 MB de memoria.

Con frecuencia es necesario ayudar al GC. Desafortunadamente, GC. AddMemoryPressure() y GC. RemoveMemoryPressure() no se admite, por lo que si sabe que acaba de liberar un grafo de objetos asignados a Java grande, puede que tenga que llamar manualmente a GC. Collect() para solicitar a una GC que libere la memoria del lado Java o puede eliminar explícitamente las subclases Java.Lang.Object , lo que rompe la asignación entre el contenedor que se puede llamar administrado y la instancia de Java.

Nota:

Debe tener mucho cuidado al eliminar las instancias de Java.Lang.Object subclase.

Para minimizar la posibilidad de daños en la memoria, observe las instrucciones siguientes al llamar a Dispose().

Uso compartido entre varios subprocesos

Si la instancia de Java o la instancia administrada se pueden compartir entre varios subprocesos, no debería ser Dispose()d, nunca. Por ejemplo: Typeface.Create() puede devolver una instancia almacenada en caché. Si varios subprocesos proporcionan los mismos argumentos, obtendrán la misma instancia. Por lo tanto, Dispose()la creación de la Typeface instancia de un subproceso puede invalidar otros subprocesos, lo que puede dar ArgumentExceptionlugar a s de JNIEnv.CallVoidMethod() (entre otros) porque la instancia se ha eliminado de otro subproceso.

Eliminación de tipos de Java enlazados

Si la instancia es de un tipo de Java enlazado, la instancia se puede eliminar siempre y cuando la instancia no se vuelva a usar desde código administrado y la instancia de Java no se pueda compartir entre subprocesos (consulte la explicación anterior Typeface.Create() ). (Hacer esta determinación puede ser difícil). La próxima vez que la instancia de Java escriba código administrado, se creará un nuevo contenedor para ella.

Esto suele ser útil cuando se trata de drawables y otras instancias con muchos recursos:

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

Lo anterior es seguro porque el elemento Peer que drawable.CreateFromPath() devuelve hará referencia a un elemento del mismo nivel framework, no a un elemento del mismo nivel de usuario. La Dispose() llamada al final del using bloque interrumpirá la relación entre las instancias drawable administradas y drawable del marco, lo que permite recopilar la instancia de Java tan pronto como necesite android runtime. Esto no sería seguro si la instancia del mismo nivel hace referencia a un elemento del mismo nivel de usuario; aquí se usa información "externa" para saber que no Drawable puede hacer referencia a un elemento del mismo nivel de usuario y, por tanto, la Dispose() llamada es segura.

Eliminación de otros tipos

Si la instancia hace referencia a un tipo que no es un enlace de un tipo de Java (por ejemplo, un personalizado Activity), NO llame Dispose() a a menos que sepa que ningún código Java llamará a métodos invalidados en esa instancia. Si no lo hace, se produce sNotSupportedException.

Por ejemplo, si tiene un agente de escucha de clic personalizado:

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

No debe eliminar esta instancia, ya que Java intentará invocar métodos en ella en el futuro:

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

Usar comprobaciones explícitas para evitar excepciones

Si ha implementado un método de sobrecarga Java.Lang.Object.Dispose , evite tocar objetos que implican JNI. Si lo hace, puede crear una situación de eliminación doble que permita que el código intente acceder a un objeto Java subyacente que ya se ha recopilado como elemento no utilizado. Si lo hace, se produce una excepción similar a la siguiente:

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

Esta situación se produce a menudo cuando la primera eliminación de un objeto hace que un miembro se convierta en NULL y, a continuación, un intento de acceso posterior en este miembro nulo hace que se produzca una excepción. En concreto, el objeto Handle (que vincula una instancia administrada a su instancia de Java subyacente) se invalida en la primera eliminación, pero el código administrado sigue intentando acceder a esta instancia de Java subyacente aunque ya no esté disponible (consulte Contenedores invocables administrados para obtener más información sobre la asignación entre instancias de Java e instancias administradas).

Una buena manera de evitar esta excepción es comprobar explícitamente en Dispose el método que la asignación entre la instancia administrada y la instancia de Java subyacente sigue siendo válida; es decir, compruebe si el objeto Handle es null (IntPtr.Zero) antes de acceder a sus miembros. Por ejemplo, el método siguiente Dispose tiene acceso a un childViews objeto :

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 un pase de eliminación inicial hace que childViews tenga un valor no válido Handle, el acceso al for bucle producirá una ArgumentExceptionexcepción . Al agregar una comprobación explícita Handle nula antes del primer childViews acceso, el método siguiente Dispose impide que se produzca la excepción:

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

Reducir instancias a las que se hace referencia

Cada vez que se examina una instancia de un Java.Lang.Object tipo o subclase durante el GC, también se debe examinar todo el gráfico de objetos al que hace referencia la instancia. El gráfico de objetos es el conjunto de instancias de objeto a las que hace referencia la "instancia raíz", además de todo lo que hace referencia la instancia raíz, de forma recursiva.

Observa la clase siguiente:

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

Cuando BadActivity se construye, el gráfico de objetos contendrá 10004 instancias (1x BadActivity, 1x , 1x strings, 1x string[] que mantienen strings, 10000x instancias de cadena), todas las cuales tendrán que examinarse cada vez que se examine la BadActivity instancia.

Esto puede afectar negativamente a los tiempos de recopilación, lo que da lugar a un aumento de los tiempos de pausa de GC.

Puede ayudar al GC mediante la reducción del tamaño de los gráficos de objetos que están rooteados por instancias del mismo nivel de usuario. En el ejemplo anterior, esto se puede hacer moviendo BadActivity.strings a una clase independiente que no hereda 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)));
    }
}

Colecciones secundarias

Las colecciones secundarias se pueden realizar manualmente mediante una llamada a GC. Collect(0). Las colecciones secundarias son baratas (en comparación con las colecciones principales), pero tienen un costo fijo significativo, por lo que no desea desencadenarlas con demasiada frecuencia y deben tener un tiempo de pausa de unos pocos milisegundos.

Si la aplicación tiene un "ciclo de trabajo" en el que se hace lo mismo, puede ser aconsejable realizar manualmente una recolección menor una vez finalizado el ciclo de trabajo. Entre los ciclos de trabajo de ejemplo se incluyen:

  • Ciclo de representación de un solo fotograma de juego.
  • Toda la interacción con un cuadro de diálogo de aplicación determinado (abrir, rellenar, cerrar)
  • Un grupo de solicitudes de red para actualizar o sincronizar datos de la aplicación.

Colecciones principales

Las colecciones principales se pueden realizar manualmente mediante una llamada a GC. Collect() o GC.Collect(GC.MaxGeneration).

Deben realizarse rara vez y pueden tener un tiempo de pausa de un segundo en un dispositivo de estilo Android al recopilar un montón de 512 MB.

Las colecciones principales solo se deben invocar manualmente, si alguna vez:

  • Al final de los ciclos de trabajo largos y cuando una pausa larga no presente un problema al usuario.

  • Dentro de un método Android.App.Activity.OnLowMemory() invalidado.

Diagnóstico

Para realizar un seguimiento de cuándo se crean y destruyen las referencias globales, puede establecer la propiedad del sistema debug.mono.log para que contenga gref o gc.

Configuración

El recolector de elementos no utilizados de Xamarin.Android se puede configurar estableciendo la MONO_GC_PARAMS variable de entorno. Las variables de entorno se pueden establecer con una acción de compilación de AndroidEnvironment.

La MONO_GC_PARAMS variable de entorno es una lista separada por comas de los parámetros siguientes:

  • nursery-size = size : establece el tamaño del vivero. El tamaño se especifica en bytes y debe ser una potencia de dos. Los sufijos k , m y g se pueden usar para especificar kilo, mega- y gigabytes, respectivamente. La guardería es la primera generación (de dos). Un vivero más grande normalmente acelerará el programa, pero obviamente usará más memoria. Tamaño de guardería predeterminado de 512 kb.

  • soft-heap-limit = size : el consumo máximo de memoria administrada de destino para la aplicación. Cuando el uso de memoria está por debajo del valor especificado, el GC se optimiza para el tiempo de ejecución (menos colecciones). Por encima de este límite, el GC está optimizado para el uso de memoria (más recopilaciones).

  • evacuation-threshold = umbral: establece el umbral de evacuación en porcentaje. El valor debe ser un entero en el intervalo de 0 a 100. El valor predeterminado es 66. Si la fase de barrido de la colección encuentra que la ocupación de un tipo de bloque de montón específico es menor que este porcentaje, realizará una recopilación de copia para ese tipo de bloque en la siguiente colección principal, restaurando así la ocupación para cerca del 100 %. Un valor de 0 desactiva la evacuación.

  • bridge-implementation = Implementación del puente: se establecerá la opción Puente de GC para ayudar a solucionar los problemas de rendimiento de GC. Hay tres valores posibles: old , new , tarjan.

  • bridge-require-precise-merge: el puente tarjan contiene una optimización que puede, en raras ocasiones, hacer que un objeto se recopile un GC después de que se convierta en basura por primera vez. La inclusión de esta opción deshabilita esa optimización, lo que hace que los GCs sean más predecibles, pero potencialmente más lentos.

Por ejemplo, para configurar la GC para que tenga un límite de tamaño de montón de 128 MB, agregue un nuevo archivo al proyecto con una acción Compilar de AndroidEnvironment con el contenido:

MONO_GC_PARAMS=soft-heap-limit=128m