Coleta de lixo

O Xamarin.Android usa o coletor de lixo Generational Simples do Mono. Trata-se de um coletor de lixo com duas gerações e um grande espaço de objetos, com dois tipos de coleções:

  • Coleções menores (coleta a pilha Gen0)
  • Principais coleções (coleta Gen1 e grandes pilhas de espaço de objetos).

Observação

Na ausência de uma coleta explícita via CG. As coleções Collect() são sob demanda, com base em alocações de heap. Este não é um sistema de contagem de referências, os objetos não serão coletados assim que não houver referências pendentes, ou quando um escopo tiver saído. O GC será executado quando o heap secundário ficar sem memória para novas alocações. Se não houver alocações, ele não será executado.

Coleções menores são baratas e frequentes, e são usadas para coletar objetos recém-alocados e mortos. Coleções secundárias são executadas após cada poucos MB de objetos alocados. Coletas secundárias podem ser executadas manualmente chamando GC. Coletar (0)

As principais coleções são caras e menos frequentes, e são usadas para recuperar todos os objetos mortos. As principais coletas são executadas quando a memória é esgotada para o tamanho de heap atual (antes de redimensionar o heap). As principais coletas podem ser executadas manualmente chamando GC. Coletar () ou chamando GC. Coletar (int) com o argumento GC. MaxGeneration.

Coleções de objetos entre VMs

Há três categorias de tipos de objeto.

  • Objetos gerenciados: tipos que não herdam de Java.Lang.Object , por exemplo , System.String. Estes são coletados normalmente pelo CG.

  • Objetos Java: tipos Java que estão presentes na VM de tempo de execução do Android, mas não expostos à VM Mono. Estes são chatos e não serão mais discutidos. Eles são coletados normalmente pela VM de tempo de execução do Android.

  • Objetos peer: tipos que implementam IJavaObject , por exemplo, todas as subclasses Java.Lang.Object e Java.Lang.Throwable . As instâncias desses tipos têm duas "metades", um peer gerenciado e um peer nativo. O peer gerenciado é uma instância da classe C#. O peer nativo é uma instância de uma classe Java dentro da VM de tempo de execução do Android, e a propriedade C# IJavaObject.Handle contém uma referência global JNI para o peer nativo.

Existem dois tipos de pares nativos:

Como há duas VMs em um processo Xamarin.Android, há dois tipos de coleta de lixo:

  • Coleções de tempo de execução do Android
  • Coleções Mono

As coleções de tempo de execução do Android operam normalmente, mas com uma ressalva: uma referência global JNI é tratada como uma raiz GC. Consequentemente, se houver uma referência global JNI mantendo um objeto VM de tempo de execução do Android, o objeto não poderá ser coletado, mesmo que seja qualificado para coleta.

As coleções Mono são onde a diversão acontece. Os objetos gerenciados são coletados normalmente. Os objetos de mesmo nível são coletados executando o seguinte processo:

  1. Todos os objetos Peer qualificados para a coleção Mono têm sua referência global JNI substituída por uma referência global fraca JNI.

  2. Uma VM GC de tempo de execução do Android é invocada. Qualquer instância de peer nativo pode ser coletada.

  3. As referências globais fracas do JNI criadas em (1) são verificadas. Se a referência fraca tiver sido coletada, o objeto Peer será coletado. Se a referência fraca não tiver sido coletada, a referência fraca será substituída por uma referência global JNI e o objeto Peer não será coletado. Nota: na API 14+, isso significa que o valor retornado de IJavaObject.Handle pode mudar após um GC.

O resultado final de tudo isso é que uma instância de um objeto Peer viverá enquanto for referenciada por código gerenciado (por exemplo, armazenado em uma static variável) ou referenciada por código Java. Além disso, o tempo de vida dos pares nativos será estendido além do que eles viveriam de outra forma, pois o par nativo não será colecionável até que o par nativo e o par gerenciado sejam colecionáveis.

Ciclos de objetos

Os objetos de mesmo nível estão logicamente presentes no tempo de execução do Android e nas VMs do Mono. Por exemplo, uma instância de peer gerenciada Android.App.Activity terá uma instância Java de mesmo nível da estrutura android.app.Activity correspondente. Pode-se esperar que todos os objetos que herdam de Java.Lang.Object tenham representações dentro de ambas as VMs.

Todos os objetos que têm representação em ambas as VMs terão tempos de vida estendidos em comparação com objetos que estão presentes apenas em uma única VM (como um System.Collections.Generic.List<int>). Chamando GC. Collect não necessariamente coletará esses objetos, pois o Xamarin.Android GC precisa garantir que o objeto não seja referenciado por nenhuma VM antes de coletá-lo.

Para encurtar o tempo de vida do objeto, Java.Lang.Object.Dispose() deve ser invocado. Isso "cortará" manualmente a conexão no objeto entre as duas VMs, liberando a referência global, permitindo que os objetos sejam coletados mais rapidamente.

Cobranças Automáticas

A partir da versão 4.1.0, o Xamarin.Android executa automaticamente um GC completo quando um limite gref é ultrapassado. Este limite é de 90% do máximo conhecido de grefs para a plataforma: 1800 grefs no emulador (máximo de 2000) e 46800 grefs no hardware (máximo de 52000). Nota: Xamarin.Android só conta os grefs criados por Android.Runtime.JNIEnv, e não saberá sobre quaisquer outros grefs criados no processo. Esta é apenas uma heurística.

Quando uma coleta automática é executada, uma mensagem semelhante à seguinte será impressa no log de depuração:

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

A ocorrência disso é não-determinística, e pode acontecer em momentos inoportunos (por exemplo, no meio da renderização de gráficos). Se você vir essa mensagem, convém executar uma coleção explícita em outro lugar ou tentar reduzir o tempo de vida de objetos pares.

Opções de ponte GC

Xamarin.Android oferece gerenciamento de memória transparente com Android e o tempo de execução Android. Ele é implementado como uma extensão do coletor de lixo Mono chamado de Ponte GC.

O GC Bridge funciona durante uma coleta de lixo Mono e descobre quais objetos de mesmo nível precisam de sua "vivacidade" verificada com o heap de tempo de execução do Android. O GC Bridge faz essa determinação executando as seguintes etapas (em ordem):

  1. Induza o gráfico de referência mono de objetos peer inalcançáveis nos objetos Java que eles representam.

  2. Execute um Java GC.

  3. Verifique quais objetos estão realmente mortos.

Esse processo complicado é o que permite que subclasses de referenciar Java.Lang.Object livremente quaisquer objetos, ele remove quaisquer restrições sobre quais objetos Java podem ser vinculados ao C#. Devido a essa complexidade, o processo de ponte pode ser muito caro e pode causar pausas perceptíveis em um aplicativo. Se o aplicativo estiver enfrentando pausas significativas, vale a pena investigar uma das três implementações do GC Bridge a seguir:

  • Tarjan - Um projeto completamente novo da ponte GC baseado no algoritmo de Robert Tarjan e propagação de referência para trás. Ele tem o melhor desempenho sob nossas cargas de trabalho simuladas, mas também tem a maior parcela de código experimental.

  • Novo - Uma grande revisão do código original, corrigindo duas instâncias de comportamento quadrático, mas mantendo o algoritmo principal (baseado no algoritmo de Kosaraju para encontrar componentes fortemente conectados).

  • Antigo - A implementação original (considerada a mais estável das três). Esta é a ponte que um aplicativo deve usar se as GC_BRIDGE pausas forem aceitáveis.

A única maneira de descobrir qual GC Bridge funciona melhor é experimentando em um aplicativo e analisando a saída. Há duas maneiras de coletar os dados para benchmarking:

  • Habilitar log - Habilite o log (conforme descrito na seção Configuração ) para cada opção do GC Bridge e, em seguida, capture e compare as saídas de log de cada configuração. Inspecione as GC mensagens de cada opção, em particular, as GC_BRIDGE mensagens. Pausas de até 150ms para aplicativos não interativos são toleráveis, mas pausas acima de 60ms para aplicativos muito interativos (como jogos) são um problema.

  • Habilitar contabilidade de ponte - A contabilidade de ponte exibirá o custo médio dos objetos apontados por cada objeto envolvido no processo de ponte. Classificar essas informações por tamanho fornecerá dicas sobre o que está contendo a maior quantidade de objetos extras.

A configuração padrão é Tarjan. Se você encontrar uma regressão, talvez seja necessário definir essa opção como Antigo. Além disso, você pode optar por usar a opção Old mais estável se o Tarjan não produzir uma melhoria no desempenho.

Para especificar qual GC_BRIDGE opção um aplicativo deve usar, passar bridge-implementation=oldbridge-implementation=new ou bridge-implementation=tarjan para a MONO_GC_PARAMS variável de ambiente. Isso é feito adicionando um novo arquivo ao seu projeto com uma ação Build de AndroidEnvironment. Por exemplo:

MONO_GC_PARAMS=bridge-implementation=tarjan

Para obter mais informações, confira Configuração.

Ajudando o GC

Há várias maneiras de ajudar o GC a reduzir o uso de memória e os tempos de coleta.

Eliminando instâncias de mesmo nível

O GC tem uma exibição incompleta do processo e pode não ser executado quando a memória está baixa porque o GC não sabe que a memória está baixa.

Por exemplo, uma instância de um tipo Java.Lang.Object ou tipo derivado tem pelo menos 20 bytes de tamanho (sujeito a alterações sem aviso prévio, etc., etc.). Os Wrappers Chamáveis Gerenciados não adicionam membros de instância adicionais, portanto, quando você tiver uma instância Android.Graphics.Bitmap que se refere a um blob de 10 MB de memória, o GC do Xamarin.Android não saberá disso – o GC verá um objeto de 20 bytes e não poderá determinar se ele está vinculado a objetos alocados em tempo de execução do Android que mantêm 10 MB de memória ativa.

Muitas vezes é necessário auxiliar o AG. Infelizmente, GC. AddMemoryPressure() e GC. RemoveMemoryPressure() não são suportados, portanto, se você souber que acabou de liberar um grande gráfico de objeto alocado em Java, talvez seja necessário chamar manualmente o GC. Collect() para solicitar que um GC libere a memória do lado Java, ou você pode descartar explicitamente as subclasses Java.Lang.Object, quebrando o mapeamento entre o wrapper chamável gerenciado e a instância Java.

Observação

Você deve ser extremamente cuidadoso ao descartar instâncias de Java.Lang.Object subclasse.

Para minimizar a possibilidade de corrupção de memória, observe as seguintes diretrizes ao chamar Dispose()o .

Compartilhamento entre vários threads

Se o Java ou a instância gerenciada pode ser compartilhada entre vários threads, ela não deve ser Dispose()d, nunca. Por exemplo, Typeface.Create() pode retornar uma instância em cache. Se vários threads fornecerem os mesmos argumentos, eles obterão a mesma instância. Consequentemente, Dispose()ing da Typeface instância de um thread pode invalidar outros threads, o que pode resultar em ArgumentExceptions de (entre outros) porque a instância foi descartada JNIEnv.CallVoidMethod() de outro thread.

Descartando tipos Java vinculados

Se a instância for de um tipo Java vinculado, a instância poderá ser descartada desde que a instância não seja reutilizada do código gerenciado e a instância Java não possa ser compartilhada entre threads (consulte a discussão anterior Typeface.Create() ). (Fazer essa determinação pode ser difícil.) Na próxima vez que a instância Java inserir código gerenciado, um novo wrapper será criado para ela.

Isso é frequentemente útil quando se trata de Drawables e outras instâncias com muitos recursos:

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

O acima é seguro porque o Peer que Drawable.CreateFromPath() retorna fará referência a um peer do Framework, não a um peer do User. A Dispose() chamada no final do bloco quebrará a relação entre as instâncias Drawable gerenciadas e Framework Drawable, permitindo que a instância Java seja coletada assim que o tempo de using execução do Android precisar. Isso não seria seguro se a instância Peer se referisse a um par de usuário, aqui estamos usando informações "externas" para saber que o Drawable não pode se referir a um peer de usuário e, portanto, a Dispose() chamada é segura.

Descartando outros tipos

Se a instância se referir a um tipo que não é uma associação de um tipo Java (como um Activitypersonalizado), NÃO chame Dispose() a menos que você saiba que nenhum código Java chamará métodos substituídos nessa instância. Não fazer isso resulta em NotSupportedExceptions.

Por exemplo, se você tiver um ouvinte de clique personalizado:

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

Você não deve descartar essa instância, pois o Java tentará invocar métodos nela no futuro:

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

Usando verificações explícitas para evitar exceções

Se você implementou um método de sobrecarga Java.Lang.Object.Dispos , evite tocar em objetos que envolvam JNI. Isso pode criar uma situação de descarte duplo que possibilita que seu código tente (fatalmente) acessar um objeto Java subjacente que já foi coletado no lixo. Isso produz uma exceção semelhante à seguinte:

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

Essa situação geralmente ocorre quando o primeiro descarte de um objeto faz com que um membro se torne nulo e, em seguida, uma tentativa de acesso subsequente sobre esse membro nulo faz com que uma exceção seja lançada. Especificamente, o objeto Handle (que vincula uma instância gerenciada à sua instância Java subjacente) é invalidado na primeira eliminação, mas o código gerenciado ainda tenta acessar essa instância Java subjacente mesmo que ela não esteja mais disponível (consulte Managed Callable Wrappers para obter mais informações sobre o mapeamento entre instâncias Java e instâncias gerenciadas).

Uma boa maneira de evitar essa exceção é verificar explicitamente em seu Dispose método se o mapeamento entre a instância gerenciada e a instância Java subjacente ainda é válido, ou seja, verifique se o objeto é nulo (IntPtr.Zero) antes de Handle acessar seus membros. Por exemplo, o seguinte Dispose método acessa um 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)
        {
            // ...
        }
    }
}

Se um passe de descarte inicial fizer com childViews que um Handle, o acesso de for loop lançará um ArgumentExceptionarquivo . Ao adicionar uma verificação nula explícita Handle antes do primeiro childViews acesso, o seguinte Dispose método impede que a exceção ocorra:

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

Reduzir instâncias referenciadas

Sempre que uma instância de um Java.Lang.Object tipo ou subclasse é verificada durante o GC, todo o gráfico de objeto ao qual a instância se refere também deve ser verificado. O gráfico de objeto é o conjunto de instâncias de objeto ao qual a "instância raiz" se refere, além de tudo referenciado pelo que a instância raiz se refere, recursivamente.

Considere a classe a seguir:

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 for construído, o gráfico de objeto conterá 10004 instâncias (1x BadActivity, 1x strings, 1x string[] mantidas por strings, 10000x instâncias de cadeia de caracteres), todas as quais precisarão ser verificadas sempre que a BadActivity instância for verificada.

Isso pode ter impactos prejudiciais em seus tempos de coleta, resultando em tempos de pausa de GC aumentados.

Você pode ajudar o GC reduzindo o tamanho dos gráficos de objeto que são enraizados por instâncias de mesmo nível de usuário. No exemplo acima, isso pode ser feito movendo-se BadActivity.strings para uma classe separada que não herda 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)));
    }
}

Coleções Menores

Coletas secundárias podem ser executadas manualmente chamando GC. Collect(0). As coleções menores são baratas (quando comparadas às coleções maiores), mas têm um custo fixo significativo, então você não quer acioná-las com muita frequência e deve ter um tempo de pausa de alguns milissegundos.

Se o seu aplicativo tiver um "ciclo de trabalho" no qual a mesma coisa é feita repetidamente, pode ser aconselhável executar manualmente uma pequena coleta uma vez que o ciclo de trabalho tenha terminado. Exemplos de ciclos de trabalho incluem:

  • O ciclo de renderização de um único quadro de jogo.
  • Toda a interação com uma determinada caixa de diálogo do aplicativo (abertura, preenchimento, fechamento)
  • Um grupo de solicitações de rede para atualizar/sincronizar dados do aplicativo.

Principais Coleções

As principais coletas podem ser executadas manualmente chamando GC. Collect() ou GC.Collect(GC.MaxGeneration).

Eles devem ser realizados raramente, e podem ter um tempo de pausa de um segundo em um dispositivo estilo Android ao coletar um heap de 512MB.

As principais coleções só devem ser invocadas manualmente, se houver:

  • No final de longos ciclos de trabalho e quando uma longa pausa não apresentará um problema para o usuário.

  • Dentro de um método Android.App.Activity.OnLowMemory() substituído.

Diagnósticos

Para controlar quando as referências globais são criadas e destruídas, você pode definir a propriedade debug.mono.log system para conter gref e/ou gc.

Configuração

O coletor de lixo Xamarin.Android pode ser configurado definindo a MONO_GC_PARAMS variável de ambiente. As variáveis de ambiente podem ser definidas com uma ação de compilação do AndroidEnvironment.

A MONO_GC_PARAMS variável de ambiente é uma lista separada por vírgulas dos seguintes parâmetros:

  • nursery-size = tamanho : Define o tamanho do viveiro. O tamanho é especificado em bytes e deve ser uma potência de dois. Os sufixos k , m e g podem ser usados para especificar kilo-, mega- e gigabytes, respectivamente. O viveiro é a primeira geração (de duas). Um berçário maior geralmente acelerará o programa, mas obviamente usará mais memória. O tamanho padrão do berçário 512 kb.

  • soft-heap-limit = size : O consumo máximo de memória gerenciada de destino para o aplicativo. Quando o uso de memória está abaixo do valor especificado, o GC é otimizado para tempo de execução (menos coleções). Acima desse limite, o GC é otimizado para uso de memória (mais coleções).

  • evacuation-threshold = threshold : Define o limite de evacuação em porcentagem. O valor deve ser um inteiro no intervalo de 0 a 100. O padrão é 66. Se a fase de varredura da coleção descobrir que a ocupação de um tipo de bloco de heap específico é menor do que essa porcentagem, ela fará uma coleta de cópia para esse tipo de bloco na próxima coleção principal, restaurando assim a ocupação para perto de 100%. Um valor de 0 desativa a evacuação.

  • bridge-implementation = implementação de ponte: Isso definirá a opção GC Bridge para ajudar a resolver problemas de desempenho GC. Há três valores possíveis: antigo , novo , tarjan.

  • bridge-require-precise-merge: A ponte Tarjan contém uma otimização que pode, em raras ocasiões, fazer com que um objeto seja coletado um GC depois que ele se torna lixo pela primeira vez. A inclusão dessa opção desabilita essa otimização, tornando os GCs mais previsíveis, mas potencialmente mais lentos.

Por exemplo, para configurar o GC para ter um limite de tamanho de heap de 128MB, adicione um novo arquivo ao seu projeto com uma ação Build de AndroidEnvironment com o conteúdo:

MONO_GC_PARAMS=soft-heap-limit=128m