В этой статье мы поговорим о взаимодействии среды выполнения IL2CPP со сборщиком мусора и увидим, каким образом корни сборщика мусора в управляемом коде связываются с нативным сборщиком мусора.



Предыдущие материалы темы:

IL2CPP: Обертки P/Invoke для типов и методов
IL2CPP: вызовы методов

В этой статье, как и в предыдущих публикациях серии, мы раскроем детали реализации отдельного компонента IL2CPP, которые могут быть изменены в будущем. Рассмотрим некоторые внутренние API, используемые кодом среды выполнения для взаимодействия со сборщиком мусора. Эти API не являются публичными, так что не следует пытаться вызывать их из кода какого-нибудь реального проекта.

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


Я не буду приводить здесь общую информацию о сборке мусора, так как это довольно широкая тема, которой посвящено много исследований и публикаций. Для краткости представим сборщик мусора в виде алгоритма, который занимается построением направленных графов из ссылок на объекты. Если объект Child используется объектом Parent посредством указателя в нативном коде, то граф будет иметь такой вид:



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

Конечно, большинство объектов должны иметь какой-нибудь родительский объект. Поэтому сборщику мусора нужно уметь отличать особые родительские объекты. Я предпочитаю думать о последних, как об объектах, используемых программой. В терминологии сборщика мусора они называются «корнями». Ниже приведен пример родительского объекта без корня.



В данном случае у объекта Parent 2 нет корня, поэтому сборщик мусора может повторно использовать память, занимаемую объектами Parent 2 и Child 2. В свою очередь, Parent 1 и Child 1 имеют корень – а значит они используются программой, и сборщик мусора не будет повторно использовать их память, так как программа всё еще использует их для определенной цели.

В .NET используются корни трех видов:

  • локальные переменные в стеке любого потока, выполняющего управляемый код;
  • статические переменные;
  • объекты GCHandle.

Мы рассмотрим взаимоотношения IL2CPP со сборщиком мусора при работе с корнями всех перечисленных выше видов.

Подготовка к работе


Я работаю в Unity версии 5.1.0p1 на OSX, и буду осуществлять сборку для платформы iOS. Это позволит нам использовать Xcode для наблюдения за взаимодействием IL2CPP со сборщиком мусора. Как и в предыдущих примерах, мы будем использовать проект, содержащий один скрипт:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;

public class AnyClass {}

public class HelloWorld : MonoBehaviour {
private static AnyClass staticAnyClass = new AnyClass();
void Start () {
var thread = new Thread(AnotherThread);
thread.Start();
thread.Join();
var anyClassForGCHandle = new AnyClass();
var gcHandle = GCHandle.Alloc(anyClassForGCHandle);
}

private static void AnotherThread() {
var anyClassLocal = new AnyClass();
}
}

Я отметил опцию Development Build в окне Build Settings, и выбрал Debug напротив Run in Xcode as. В сгенерированном проекте Xcode прежде всего найдите строку Start_m. Вы должны увидеть сгенерированный код для метода Start класса HelloWorld под названием HelloWorld_Start_m3.

Добавляем потоковые локальные переменные в качестве корней


Добавим точку останова в функции HelloWorld_Start_m3 на строке, где вызывается Thread_Start_m9. Этот метод создает новый управляемый поток, который будет добавлен в качестве корня в сборщик мусора. Этот процесс можно отследить в заголовочных файлах libil2cpp, поставляемых вместе с Unity. В директории установки Unity откройте файл Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h. Он содержит ряд методов с префиксом il2cpp_gc_ и является частью API между средой выполнения libil2cpp и сборщиком мусора. Но помните, что этот API общедоступен, потому данные методы не следует вызывать из кода реального проекта. К тому же они могут быть изменены в новой версии без уведомления.

Добавим точку останова в функции il2cpp_gc_register_thread в Xcode. Для этого нужно выбрать Debug > Breakpoints > Create Symbolic Breakpoint.



Эта точка достигается практически мгновенно после запуска проекта в Xcode. В данном случае мы не видим исходного кода, так как он построен в статической библиотеке среды libil2cpp, однако из стека вызовов ясно, что этот поток создается в методе InitializeScriptingBackend, который выполняется при запуске.



Мы увидим, что эта точка будет достигаться несколько раз по мере создания управляемых потоков для внутреннего использования. Пока что ее можно отключить в Xcode и продолжить выполнение проекта без нее. Мы должны достичь точку останова, которая была добавлена ранее в методе HelloWorld_Start_m3.

Теперь я собираюсь запустить управляемый поток, созданный нашим скриптовым кодом, поэтому нужно снова включить точку останова на il2cpp_gc_register_thread. Достигнув ее, мы увидим, что первый поток ожидает присоединения к созданному потоку, но стек вызовов для созданного потока показывает, что мы только запускаем его:



Когда новый поток связывается со сборщиком мусора, последний интерпретирует все объекты в локальном стеке этого потока как корни. Взглянем на сгенерированный код для метода HelloWorld_AnotherThread_m4:

AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var);
AnyClass__ctor_m0(L_0, /*hidden argument*/NULL);
V_0 = L_0;

Мы видим одну локальную переменную L_0, которую сборщик мусора должен интерпретировать как корневую. В течение недолгого времени, пока существует этот поток, данный экземпляр объекта AnyClass и любые другие объекты, на которые он ссылается, не могут быть повторно использованы сборщиком мусора. Определенные в стеке переменные – это наиболее распространенный тип корня, так как объекты в программе преимущественно начинаются с локальной переменной в методе, исполняемом в управляемом потоке.

При завершении потока вызывается функция il2cpp_gc_unregister_thread, которая указывает сборщику мусора больше не интерпретировать объекты стека потока как корни. После этого сборщик мусора сможет повторно использовать память, занимаемую объектом класса AnyClass, который представлен в нативном коде как L_0.

Статические переменные


Некоторые переменные не зависят от потоковых стеков вызовов. Речь идет о статических переменных, и они тоже должны интерпретироваться сборщиком мусора как корневые.

Когда IL2CPP создает нативное отображение класса, все статические поля группируются в структуру C++, отличную от экземпляров полей в классе. Перейдем к определению класса HelloWorld_t2 в Xcode:

struct  HelloWorld_t2  : public MonoBehaviour_t3
{
};

struct HelloWorld_t2_StaticFields{
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2;
};

Обратите внимание, что технология IL2CPP не использует ключевое слово C++ static, поскольку она должна постоянно контролировать размещение статических полей, а также выделение памяти для них, чтобы нужным образом взаимодействовать со сборщиком мусора. Когда определенный тип впервые используется в среде выполнения, код libil2cpp выполнит инициализацию типа. Такая инициализация включает в себя выделение памяти для структуры HelloWorld_t2_StaticFields. Память выделяется с помощью специального вызова к сборщику мусора: il2cpp_gc_alloc_fixed (его можно увидеть в файле gc-internal.h).

После данного вызова сборщик мусора будет принимать выделенную память за корень до окончания процесса. Можно установить точку останова на функции il2cpp_gc_alloc_fixed в Xcode, но она вызывается довольно часто (даже в таком простом проекте, как наш), потому не будет очень полезной.

Объекты GCHandle


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

При создании объекта GCHandle код среды обработки начинает интерпретировать выбранный управляемый объект как корень в сборщике мусора, чтобы не допустить повторное использование памяти ни этого объекта, ни любого другого, на который он ссылается. В IL2CPP мы видим, как низкоуровневый API выполняет это в файле Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h. Опять же, напоминаю, что этот API не является публичным. Добавим точку останова на функцию GCHandle::New. Если мы продолжим выполнение проекта, должен появиться такой стек вызовов:



Сгенерированный код для метода Start вызывает метод GCHandle_Alloc_m11, который в итоге создает объект GCHandle и уведомляет сборщик мусора о новом корневом объекте.

Вывод


Тема интеграции сборщика мусора в IL2CPP по-прежнему далеко не исчерпана. Я настоятельно рекомендую читателям самостоятельно изучать больше материала о взаимодействии IL2CPP и сборщика мусора.
Поделиться с друзьями
-->

Комментарии (0)