Сборка мусора

Xamarin.Android использует простой сборщик мусора поколения Mono. Это сборщик мусора mark-and-sweep с двумя поколениями и большим пространством объектов с двумя типами коллекций:

  • Незначительные коллекции (собирает кучу 0-го поколения)
  • Основные коллекции (собирает кучу больших объектов и 1-го поколения).

Примечание.

В отсутствие явной коллекции через GC. Сбор () коллекций по запросу зависит от выделения кучи. Это не система подсчета ссылок; объекты не будут собираться сразу после отсутствия невыполненных ссылок или при выходе область. GC будет выполняться, когда небольшая куча не хватает памяти для новых выделений. Если выделения отсутствуют, он не будет выполняться.

Незначительные коллекции являются дешевыми и частыми и используются для сбора недавно выделенных и мертвых объектов. Небольшие коллекции выполняются каждые несколько МБ выделенных объектов. Дополнительные коллекции могут выполняться вручную путем вызова GC. Сбор (0)

Основные коллекции являются дорогостоящими и менее частыми и используются для восстановления всех мертвых объектов. Основные коллекции выполняются после исчерпания памяти для текущего размера кучи (перед изменением размера кучи). Крупные коллекции могут выполняться вручную путем вызова GC. Сбор () или вызов GC. Сбор (int) с помощью аргумента GC. MaxGeneration.

Коллекции объектов между виртуальными машинами

Существует три категории типов объектов.

  • Управляемые объекты: типы, которые не наследуются от Java.Lang.Object , например System.String. Обычно они собираются в GC.

  • Объекты Java: типы Java, которые присутствуют на виртуальной машине среды выполнения Android, но не предоставляются виртуальной машине Mono. Это скучно, и не будет обсуждаться дальше. Обычно они собираются виртуальной машиной среды выполнения Android.

  • Одноранговые объекты: типы, реализующие IJavaObject , например все подклассы Java.Lang.Object и Java.Lang.Throwable . Экземпляры этих типов имеют два "половины" управляемого однорангового узла и собственного одноранговогоузла. Управляемый одноранговый узел — это экземпляр класса C#. Собственный одноранговый узел — это экземпляр класса Java в виртуальной машине среды выполнения Android, а свойство C# IJavaObject.Handle содержит глобальную ссылку JNI на собственный одноранговый узел.

Существует два типа собственных одноранговых узлов:

  • Пиринги платформы : "Обычные" типы Java, которые не знают ничего из Xamarin.Android, например android.content.Context.

  • Пиринги пользователей: вызываемые оболочки Android, созданные во время сборки для каждого подкласса Java.Lang.Object, присутствующих в приложении.

Так как в процессе Xamarin.Android есть две виртуальные машины, существует два типа сборок мусора:

  • Коллекции среды выполнения Android
  • Коллекции Mono

Коллекции среды выполнения Android работают обычно, но с предостережением: глобальная ссылка JNI рассматривается как корень GC. Следовательно, если существует глобальная ссылка JNI на объект виртуальной машины среды выполнения Android, объект не может быть собран, даже если он в противном случае имеет право на коллекцию.

Коллекции Mono находятся в том месте, где происходит весело. Управляемые объекты обычно собираются. Одноранговые объекты собираются путем выполнения следующего процесса:

  1. Все объекты одноранговых узлов, доступные для коллекции Mono, имеют глобальную ссылку JNI, замененную слабой глобальной ссылкой JNI.

  2. Вызывается GC виртуальной машины среды выполнения Android. Может быть собран любой экземпляр собственного однорангового узла.

  3. Слабые глобальные ссылки JNI, созданные в (1), проверка. Если была собрана слабая ссылка, то собирается объект Peer. Если слабая ссылка не была собрана, то слабая ссылка заменяется глобальной ссылкой JNI, а объект Peer не собирается. Примечание. В API 14+, это означает, что возвращаемое IJavaObject.Handle значение может измениться после GC.

Конечным результатом всего этого является то, что экземпляр однорангового объекта будет жить до тех пор, пока он ссылается на управляемый код (например, хранящийся в переменной static ) или ссылается на код Java. Кроме того, время существования собственных одноранговых узлов будет расширено за рамки того, что они в противном случае будут жить, так как собственный одноранговый узел не будет собираться, пока не будет собираться как собственный, так и управляемый одноранговый узел.

Циклы объектов

Одноранговые объекты логически присутствуют как в среде выполнения Android, так и в виртуальной машине Mono. Например, управляемый экземпляр однорангового узла Android.App.Activity будет иметь соответствующий экземпляр платформы android.app.Activity . Все объекты, наследуемые от Java.Lang.Object , могут иметь представления в обеих виртуальных машинах.

Все объекты, имеющие представление в обеих виртуальных машинах, будут иметь время существования, которые расширяются по сравнению с объектами, которые присутствуют только в одной виртуальной машине (например System.Collections.Generic.List<int>, в ). Вызов GC. Сбор не обязательно собирает эти объекты, так как Xamarin.Android GC должен убедиться, что объект не ссылается на любую виртуальную машину перед сбором.

Чтобы сократить время существования объекта, необходимо вызвать Java.Lang.Object.Dispose( ). Это позволит вручную "сократить" подключение к объекту между двумя виртуальными машинами, освободив глобальную ссылку, что позволит быстрее собирать объекты.

Автоматические коллекции

Начиная с выпуска 4.1.0, Xamarin.Android автоматически выполняет полную сборку GC при пересечении порогового значения gref. Это пороговое значение составляет 90 % известных максимальных grefs для платформы: 1800 grefs в эмуляторе (максимум 2000) и 46800 grefs на оборудовании (максимум 52000). Примечание. Xamarin.Android подсчитывает только grefs, созданные Android.Runtime.JNIEnv, и не будет знать о других grefs, созданных в процессе. Это эвристическая только.

При выполнении автоматической коллекции сообщение, аналогичное следующему, будет напечатано в журнал отладки:

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

Вхождение этого недетерминированного и может произойти в неоптимальное время (например, в середине отрисовки графики). Если вы видите это сообщение, может потребоваться выполнить явную коллекцию в другом месте или попытаться сократить время существования одноранговых объектов.

Параметры моста GC

Xamarin.Android предлагает прозрачное управление памятью с помощью Android и среды выполнения Android. Он реализуется как расширение сборщика мусора Mono под названием мост GC.

Мост GC работает во время сборки мусора Mono и определяет, какие одноранговые объекты нуждаются в их "живости", проверенной кучей среды выполнения Android. Мост GC делает это определение, выполнив следующие действия (в порядке):

  1. Вызовите граф ссылок mono для недоступных одноранговых объектов в объекты Java, которые они представляют.

  2. Выполните сборку GC Java.

  3. Проверьте, какие объекты действительно мертвы.

Этот сложный процесс позволяет подклассам Java.Lang.Object свободно ссылаться на любые объекты; он удаляет любые ограничения, на которые объекты Java могут быть привязаны к C#. Из-за этой сложности процесс моста может быть очень дорогим, и это может вызвать заметные паузы в приложении. Если приложение испытывает значительные паузы, стоит исследовать одну из следующих трех реализаций моста GC:

  • Тарджан - совершенно новый дизайн моста GC на основе алгоритма Роберта Тарджана и обратного распространения ссылок. Она имеет лучшую производительность в рамках имитированных рабочих нагрузок, но также имеет большую долю экспериментального кода.

  • Новое — основной ремонт исходного кода, исправление двух экземпляров квадратного поведения, но сохранение основного алгоритма (на основе алгоритма Косараджу для поиска строго подключенных компонентов).

  • Старая — исходная реализация (считается самой стабильной из трех). Это мост, который приложение должно использовать, если GC_BRIDGE паузы допустимы.

Единственный способ выяснить, какой мост GC работает лучше всего, — экспериментировать в приложении и анализировать выходные данные. Существует два способа сбора данных для тестирования.

  • Включение ведения журнала — включение ведения журнала (как описано в разделе конфигурации ) для каждого параметра моста GC, а затем захват и сравнение выходных данных журнала из каждого параметра. GC Проверьте сообщения для каждого варианта. В частности, GC_BRIDGE сообщения. Приостановки до 150 мс для неинтерактивных приложений являются допустимыми, но паузы выше 60 мс для очень интерактивных приложений (таких как игры) являются проблемой.

  • Включение учета моста — учет моста будет отображать среднюю стоимость объектов, на которые указывает каждый объект, участвующий в процессе моста. Сортировка этих сведений по размеру будет предоставлять указания относительно того, что содержит наибольшее количество дополнительных объектов.

Значением по умолчанию является Tarjan. Если вы нашли регрессию, возможно, вам потребуется задать для этого параметра значение Old. Кроме того, вы можете использовать более стабильный старый вариант, если Tarjan не создает улучшения производительности.

Чтобы указать, какой GC_BRIDGE параметр должен использовать приложение, передать bridge-implementation=newbridge-implementation=oldили bridge-implementation=tarjan в MONO_GC_PARAMS переменную среды. Это достигается путем добавления нового файла в проект с действием сборкиAndroidEnvironment. Например:

MONO_GC_PARAMS=bridge-implementation=tarjan

Дополнительные сведения см. в статье Конфигурация.

Помощь в сборке мусора

Существует несколько способов помочь GC уменьшить время использования памяти и сбора.

Удаление одноранговых экземпляров

GC имеет неполное представление процесса и может не запускаться, если память низка, так как GC не знает, что память низка.

Например, экземпляр типа Java.Lang.Object или производного типа составляет не менее 20 байт (при условии изменения без уведомления и т. д.). Управляемые вызываемые оболочки не добавляют дополнительные члены экземпляра, поэтому если у вас есть экземпляр Android.Graphics.Bitmap, ссылающийся на 10 МБ большой двоичный объект памяти, Xamarin.Android не знает, что — GC увидит 20-байтовый объект и не сможет определить, что он связан с выделенными средой выполнения Android объектами, которые хранят 10 МБ памяти.

Часто необходимо помочь GC. К сожалению, GC. AddMemoryPressure() и GC. RemoveMemoryPressure() не поддерживается, поэтому если вы знаете , что вы только что освободили большой граф объектов, выделенный Java, может потребоваться вручную вызвать GC. Сбор() для запроса GC освободить память на стороне Java или явно удалить подклассы Java.Lang.Object , нарушая сопоставление между управляемой вызываемой оболочкой и экземпляром Java.

Примечание.

При удалении экземпляров подклассов Java.Lang.Object необходимо быть очень осторожным.

Чтобы свести к минимуму вероятность повреждения памяти, следуйте приведенным ниже рекомендациям при вызове Dispose().

Совместное использование между несколькими потоками

Если java или управляемыйэкземпляр могут быть общими для нескольких потоков, он никогда не должен быть Dispose()d. Например: Typeface.Create() может возвращать кэшированный экземпляр. Если несколько потоков предоставляют одинаковые аргументы, они получат один и тот же экземпляр. Следовательно, Dispose()использование экземпляра из одного потока может привести к недопустимости Typeface других потоков, что может привести к тому ArgumentExceptionJNIEnv.CallVoidMethod() , что экземпляр был удален из другого потока.

Удаление привязанных типов Java

Если экземпляр имеет привязанный тип Java, экземпляр может быть удален до тех пор, пока экземпляр не будет повторно использован из управляемого кода , и экземпляр Java не может использоваться между потоками (см. предыдущее Typeface.Create() обсуждение). (Сделать это определение может быть трудно.) В следующий раз, когда экземпляр Java вводит управляемый код, для него будет создана новая оболочка.

Это часто полезно, когда речь идет о рисуемых и других экземплярах с большим количеством ресурсов:

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

Приведенное выше безопасно, так как одноранговый элемент, возвращаемый Drawable.CreateFromPath(), будет ссылаться на одноранговый узел Framework, а не одноранговый узел пользователя. Вызов Dispose() в конце using блока разорвит связь между управляемыми экземплярами Drawable и платформой Drawable, что позволяет экземпляру Java собираться сразу после необходимости выполнения Android. Это не будет безопасно, если одноранговый экземпляр ссылается на одноранговый узел пользователя. Здесь мы используем "внешние" сведения, чтобы знать, что Drawable не удается ссылаться на одноранговый узел пользователя, и таким образом Dispose() вызов является безопасным.

Удаление других типов

Если экземпляр ссылается на тип, который не является привязкой типа Java (например, настраиваемого Activity), не вызывайте Dispose() , если вы не знаете , что код Java не будет вызывать переопределенные методы в этом экземпляре. Сбой этого приводит к сбоюNotSupportedException.

Например, если у вас есть пользовательский прослушиватель щелчков:

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

Этот экземпляр не следует удалять, так как Java попытается вызвать методы в будущем:

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

Использование явных проверок для предотвращения исключений

Если вы реализовали метод перегрузки Java.Lang.Object.Dispose , избегайте касания объектов, включающих JNI. Это может создать ситуацию двойного удаления , которая позволяет коду (смертельно) попытаться получить доступ к базовому объекту Java, который уже был собран мусором. При этом создается исключение, аналогичное следующему:

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

Эта ситуация часто возникает, когда первое удаление объекта приводит к тому, что член становится null, а затем последующая попытка доступа к этому элементу NULL приводит к возникновению исключения. В частности, объект Handle (который связывает управляемый экземпляр с его базовым экземпляром Java) является недействительным при первом удалении, но управляемый код по-прежнему пытается получить доступ к этому базовому экземпляру Java, несмотря на то, что он больше недоступен (дополнительные сведения о сопоставлении между экземплярами Java и управляемыми экземплярами ).

Чтобы предотвратить это исключение, можно явно проверить в Dispose методе, что сопоставление между управляемым экземпляром и базовым экземпляром Java по-прежнему допустимо; то есть проверка, чтобы узнать, имеет ли объект Handle значение NULL (IntPtr.Zero) перед доступом к его членам. Например, следующий Dispose метод обращается к объекту 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)
        {
            // ...
        }
    }
}

Если начальный проход удаления приводит childViews к недопустимому Handle, for доступ к циклу вызовет ArgumentExceptionисключение. Добавив явный Handle пустой проверка перед первым childViews доступом, следующий Dispose метод предотвращает возникновение исключения:

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

Сокращение ссылочных экземпляров

При проверке экземпляра Java.Lang.Object типа или подкласса во время сборки данных весь граф объектов, на который ссылается экземпляр, также должен быть сканирован. Граф объектов — это набор экземпляров объектов, на которые ссылается "корневой экземпляр", а также все, на что ссылается корневой экземпляр, рекурсивно.

Рассмотрим следующий класс :

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

При BadActivity построении граф объекта будет содержать 10004 экземпляров (1x, 1xBadActivity, 1xstringsstring[], удерживаемых strings1000x строковых экземпляров), все из которых необходимо проверять при BadActivity сканировании экземпляра.

Это может негативно повлиять на время сбора, что приведет к увеличению времени приостановки GC.

Вы можете помочь GC, уменьшая размер графов объектов, которые коренятся экземплярами одноранговых узлов пользователей. В приведенном выше примере это можно сделать, перейдя BadActivity.strings в отдельный класс, который не наследует от 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)));
    }
}

Дополнительные коллекции

Дополнительные коллекции могут выполняться вручную путем вызова GC. Сбор(0). Незначительные коллекции являются дешевыми (по сравнению с крупными коллекциями), но имеют значительную фиксированную стоимость, поэтому вы не хотите запускать их слишком часто, и должны иметь паузу в несколько миллисекунд.

Если у вашего приложения есть "цикл дежурства", в котором то же самое делается снова и снова, может потребоваться вручную выполнить дополнительную коллекцию после завершения цикла дежурства. Примеры циклов дежурства:

  • Цикл отрисовки одного игрового кадра.
  • Все взаимодействие с заданным диалогом приложения (открытие, заполнение, закрытие)
  • Группа сетевых запросов для обновления и синхронизации данных приложения.

Основные коллекции

Крупные коллекции могут выполняться вручную путем вызова GC. Сбор() или GC.Collect(GC.MaxGeneration).

Они должны выполняться редко, и может потребоваться время приостановки секунды на устройстве с android-стиле при сборе 512 МБ кучи.

Основные коллекции должны вызываться вручную, если когда-либо:

  • В конце длительных циклов дежурства и когда длинная пауза не будет представлять проблемы для пользователя.

  • В переопределенном методе Android.App.Activity.OnLowMemory()

Диагностика

Чтобы отслеживать создание и уничтожение глобальных ссылок, можно задать для свойства системы debug.mono.log значение gref и(или) gc.

Настройка

Сборщик мусора Xamarin.Android можно настроить, задав MONO_GC_PARAMS переменную среды. Переменные среды можно задать с действием сборки AndroidEnvironment.

Переменная MONO_GC_PARAMS среды представляет собой разделенный запятыми список следующих параметров:

  • nursery-size = размер: задает размер детского сада. Размер указывается в байтах и должен быть двумя. Суффиксы km и g могут использоваться для указания кило, мега- и гигабайтов соответственно. Детский сад является первым поколением (из двух). Больший детский сад обычно ускоряет программу, но, очевидно, будет использовать больше памяти. Размер детского сада по умолчанию — 512 КБ.

  • soft-heap-limit = размер : целевое максимальное потребление управляемой памяти для приложения. Если использование памяти ниже указанного значения, GC оптимизировано для времени выполнения (меньше коллекций). Выше этого ограничения GC оптимизирован для использования памяти (больше коллекций).

  • evacuation-threshold = пороговое значение : задает порог эвакуации в процентах. Значение должно быть целым числом в диапазоне от 0 до 100. Значение по умолчанию — 66. Если этап очистки коллекции обнаруживает, что заполнение определенного типа блока кучи меньше этого процента, оно будет выполнять копирование коллекции для этого типа блока в следующей основной коллекции, тем самым восстанавливая заполнение до 100 процентов. Значение 0 отключает эвакуацию.

  • bridge-implementation = Реализация моста. Это позволит задать параметр моста GC для решения проблем с производительностью GC. Существует три возможных значения: старый , новый , tarjan.

  • bridge-require-precise-merge: Мост Tarjan содержит оптимизацию, которая может, в редких случаях, привести к сбору объекта одной сборки мусора после того, как он сначала становится мусором. В том числе этот параметр отключает эту оптимизацию, что делает GCs более предсказуемым, но потенциально медленнее.

Например, чтобы настроить размер кучи в GC размером 128 МБ добавьте новый файл в проект с действием сборки AndroidEnvironment с содержимым:

MONO_GC_PARAMS=soft-heap-limit=128m