.NET Internals 5: Garbage collection: marking, collection and heaps compaction
Сегодня мы собираемся исследовать, как сборщик мусора (GC) освобождает память (что является его основной целью), что такое этап разметки и как уплотняются управляемые кучи для оптимизации процесса. Мы также увидим, при каких условиях запускается сборка.
Содержание:
- Поиск объектов для сборки мусора
- Дерево сборки мусора
- Недостижимые (неиспользуемые) объекты
- Освобождение памяти (сборка мусора)
- Фаза разметки
- Сборка
- Упаковка кучи
- Когда запускается GC?
Поиск объектов для сборки мусора
Как мы уже знаем, .NET не позволяет разработчикам напрямую размещать объекты в куче. Вместо этого единственное, что нужно сделать программисту, - это использовать ключевое слово new - .net framework сам позаботится об остальном (размещение объекта в соответствующей куче, его инициализация и т.д.). После этого вы можете просто забыть о своём объекте, потому что он автоматически утилизируется сборщиком мусора, как только становится не нужен. Но как GC узнает, какие объекты он может собирать?
В принципе, всё, что делает сборщик мусора, - это ищет объекты, на которые ничто не ссылается, а это означает, что они больше не нужны. Это делает такие объекты готовыми к сбору (их память может быть освобождена - помечена как возможная для повторного использования)
Существует несколько источников ссылок на объекты, которые размещены в куче:
- Стек
- Глобальные или статические ссылочные переменные
- Регистры CPU
- Ссылки на взаимодействие (.net-объекты, использующие вызовы COM/API)
- Финализаторы
Дерево сборки мусора
Все вышеперечисленные источники называются корнями GC (корневыми ссылками). Поскольку объекты, на которые ссылается любой из корнец, могут ссылаться на другие объекты, все эти вложенные ссылки представлены в виде дерева (графа) для каждого .net-приложения. Такое дерево ссылок может выглядеть следующим образом:

Как вы можете видеть, корень GC присутствует в стеке, указывая на объект ссылки на клиента. Он содержит коллекцию ArrayList заказов, на которую ссылается объект Customer. Сама коллекция также содержит ссылки на ее элементы, поэтому дерево растет.
Каждый корень содержит либо ссылку (адрес) на какой-либо объект, либо нулевое значение.
Недостижимые (неиспользуемые) объекты
Дерево корней GC, описанное выше, используется сборщиком мусора для определения того, какие объекты больше не нужны. Как мы видим, график содержит все объекты, которые все используются (на которые ссылается что-то другое).
GC использует эту информацию наоборот – как правило, GC классифицирует все объекты, которые недоступны ни из какого корня (не присутствуют на графике), как мусор. Затем такие объекты собираются (освобождается выделенная для них память) GC во время циклов сбора, которые будут подробно описаны в следующих публикациях серии.
Освобождение памяти (сборка мусора)
Как уже упоминалось, сборка мусора - это процесс, выполняемый циклически, который будет рассмотрен в последующих статьях, но сам процесс содержит два основных этапа:
- Этап маркировки - поиск объектов которые всё ещё используются
- Сборка - фактический процесс сбора больше не используемых объектов (включая уплотнение SOH).
Давайте рассмотрим оба этапа в деталях
Этап маркировки
Когда сборщик мусора запускает процесс освобождения памяти (называемый сборкой), ему необходимо знать, какие объекты всё ещё используются приложением - очевидно, они не могут быть собраны.
Чтобы получить эту информацию, GC проходит через всё корневое дерево ссылок и для каждого корня перемещается по всему ссылочному дереву (всем объектам, на которые ссылается объект из определённого корня), чтобы пометить каждый объект в этом дереве на "всё ещё используемый". Вот почему этот этап сборки мусора называется этапом маркировки.
Маркировка - это рекурсивная информация, потому что, как мы уже говорили, корни ссылаются на какой-то объект, который ссылается на другой объект, который ссылается на другой объект...
Поэтому, чтобы убедиться, что каждый используемый объект корректно помечен, эта рекурсивная маркировка выполняется следующим образом:
void Mark(objectRef o)
{
if (!InUseList.Exists(o))
{
InUseList.Add(o);
List refs = GetAllChildReferences(o);
foreach (objectRef childRef in refs)
{
Mark(childRef);
}
}
}
// source: Under the Hood of .NET Memory Management
Сборка мусора
Фактический процесс сбора, включая ранее рассмотренную фазу маркировки, можно представить со следующим псевдокодом:
void Collect()
{
List gcRoots = GetAllGCRoots();
foreach (objectRef root in gcRoots)
{
Mark(root);
}
Cleanup();
}
// source: Under the Hood of .NET Memory Management
Прежде всего, GC получает список всех корней GC в приложении (список поддерживается JIT-компилятором и средой выполнения). Затем ему нужно выполнить итерацию по каждому корню и выполнить операцию маркировки на нем. Зная, что маркировка является рекурсивной операцией, после завершения этой фазы GC может быть уверен, что все объекты, имеющие любую корневую ссылку (либо прямую, либо на которые указывают другие объекты), добавляются в список используемых объектов.
Следующим шагом является фактический процесс освобождения памяти неиспользуемых объектов, представленный в виде вызова метода Cleanup() в приведенном выше коде. Очистка памяти является очень простым процессом – это просто пометка определенного блока(блоков) памяти как свободного (неиспользуемого). Там нет перезаписи памяти или чего – то подобного 🙂 (кроме "боковой перезаписи", которая происходит во время уплотнения кучи - описано ниже). Для блоков памяти используется простая булевая терминология - используется (1) или не используется (0). Это работает так просто для LOH , но коллекция на SOH (все еще не знаете, что такое LOH и SOH? Идите и читайте!) также включает в себя процесс уплотнения. Давайте посмотрим, что это такое.
Уплотнение кучи
Как мы знаем из первого поста, память на управляемых кучах может фрагментироваться, оставляя дыры неиспользуемой памяти между живыми объектами. Вот почему сбор мусора для процесса SOH включает в себя уплотнение, которое представляет собой процесс удаления дыр в памяти в куче. Это часто называют процессом сбора копий, потому что он основан на поиске мертвых объектов или дыр в памяти (на самом деле это одно и то же – дыры в памяти, которые ничем не используются, присутствуют между живыми объектами) и перезаписи этих дыр живыми объектами, выделенными позже (далее) в куче - путем копирования фрагментов живых объектов "вниз в кучу".
Результатом процесса уплотнения является непрерывный SOH, в котором объекты располагаются друг над другом без или с минимально возможным количеством отверстий в памяти.
Наконец, адреса объектов, хранящихся в ссылочных переменных, обновляются, чтобы указать на правильные, новые места в памяти, занимаемые конкретным объектом.
Основное преимущество, которое мы получаем от уплотнения, заключается в том, что SOH не фрагментирован, поэтому память может использоваться эффективно. С другой стороны, это требует некоторого процессорного времени, что может оказать некоторое влияние на производительность нашего приложения.
По умолчанию LOH не уплотняется из-за размера объектов, хранящихся на нем (>85 килобайт). Копирование фрагментов таких объектов заняло бы слишком много времени. Вместо этого отслеживается используемое пространство и пробелы в памяти в LOH, и алгоритм распределения настраивается таким образом, чтобы попытаться оптимальным образом зарезервировать память для новых объектов (путем поиска наилучших возможных свободных слотов во фрагментированной куче).
Что стоит отметить, так это то, что, начиная с .NET Framework 4.5.1, уплотнение LOH может быть включено по требованию с помощью свойства GCSettings.LargeObjectHeapCompactionMode.
Когда запускается сборщик мусора?
Можно утверждать, что GC работает недетерминированным образом. Он выполняется в отдельном потоке, когда выполняется одно из следующих условий:
- Когда операционная система отправляет уведомление о нехватке памяти,
- Когда объем памяти, используемой объектами в управляемой куче, превышает некоторый определенный порог (который динамически настраивается во время выполнения на основе текущих распределений).,
- Когда метод
GC.Collect()вызван в коде
Если вы увидите метод GC.Collect(), непосредственно используемый в коде, почти в каждом случае это означает, что с реализацией приложения что-то не так. Несколько замечательных инженеров Microsoft и авторов с открытым исходным кодом работали над автоматической сборкой мусора, чтобы не запускать ее вручную🙂
Исключения из этого правила, которые я бы принял, возможно, являются некоторые игровые реализации, в которых вы работаете с огромными объектами, и иногда вы хотели бы повлиять на то, как память этих объектов очищается или используется при отладке - большинство профилировщиков памяти (например, dotMemory или ANTS) принудительно собирают данные до того, как пользователь сделает снимок памяти. Это разумно, потому что при отладке утечек или проблем с памятью вы хотите видеть, какие объекты остаются выделенными после очистки GC.
Что также стоит упомянуть, так это то, что при запуске сборки мусора по умолчанию останавливаются все потоки, кроме потока, в котором была запущена сборка. Однако это можно настроить, изменив настройки GC для работы в режиме рабочей станции или сервера, а также заставив его работать одновременно или в фоновом режиме. Существуют различные варианты использования, в которых следует использовать каждую комбинацию настроек – вы можете найти более подробную информацию об этом здесь и здесь.