Как работают поколения в Garbage Collector?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Принцип работы поколений (Generations) в сборщике мусора Unity
В Unity используется сборщик мусора (Garbage Collector, GC) на основе алгоритма поколений (Generational Garbage Collection), который оптимизирует процесс освобождения памяти за счёт разделения объектов по "возрасту". Этот подход основан на эмпирическом наблюдении, известном как гипотеза о поколениях: большинство объектов в управляемой куче становятся мусором очень быстро (например, временные переменные в методе), в то время как выжившие объекты часто живут долго.
Три поколения объектов
Память в управляемой куче разделяется на три поколения:
-
Поколение 0 (Gen0): Здесь размещаются новые, недавно созданные объекты. Это самое маленькое по размеру поколение. Сбор мусора в Gen0 происходит наиболее часто, так как предполагается, что большинство объектов здесь — кратковременные. После создания объект всегда попадает в Gen0.
-
Поколение 1 (Gen1): Это буферная зона между Gen0 и Gen2. Объекты, которые пережили одну сборку Gen0, продвигаются (promoted) в Gen1. Размер Gen1 также относительно невелик. Сборки в Gen1 происходят реже, чем в Gen0.
-
Поколение 2 (Gen2): Здесь хранятся долгоживущие объекты. Объекты, пережившие сборки в Gen1, перемещаются в Gen2. Это поколение может занимать значительный объём памяти, так как здесь накапливаются объекты, нужные на протяжении всего жизненного цикла приложения (например, синглтоны, загруженные ассеты). Полная сборка Gen2 (Full GC) — наиболее ресурсоёмкая операция, вызывающая заметные фризы (зависания).
Жизненный цикл объекта
void ExampleMethod() {
// Объект создаётся в Generation 0
var temporaryList = new List<Vector3>(100);
// Использование объекта...
temporaryList.Add(transform.position);
// Когда метод завершается, ссылка 'temporaryList' выходит из области видимости.
// Сам объект List становится кандидатом на удаление.
}
// При следующей сборке мусора в Gen0:
// 1. Временный List будет помечен как мусор и удалён (если на него нет других ссылок).
// 2. Если бы на него осталась ссылка (например, в поле класса), он бы выжил и был продвинут в Gen1.
Процесс сборки мусора
- Запуск. GC обычно запускается, когда заканчивается память в Gen0 или при явном вызове
System.GC.Collect()(который в Unity рекомендуется избегать). - Фаза маркировки (Mark). GC начинает обход "корней" (статические поля, локальные переменные в стеке, регистры) и помечает все достижимые объекты как "живые".
- Фаза очистки (Sweep). Память, занятая непомеченными ("мёртвыми") объектами, освобождается.
- Фаза уплотнения (Compact). (Опционально, чаще в Gen2) Выжившие объекты сдвигаются вместе, чтобы устранить фрагментацию и освободить непрерывный блок памяти.
- Продвижение (Promotion). Выжившие объекты из Gen0 перемещаются в Gen1, а из Gen1 — в Gen2.
Ключевая оптимизация: При сборке конкретного поколения (например, Gen0) GC проверяет только это поколение и все младшие. Так, сборка Gen1 затрагивает объекты в Gen0 и Gen1, но не трогает Gen2, что экономит время.
Практические последствия для разработчика в Unity
- Частые аллокации в Gen0 — триггер для GC. Создание новых объектов в циклах (
Update,FixedUpdate) ведёт к быстрому заполнению Gen0 и частым, хоть и быстрым, сборкам.// ПЛОХО: Создаёт новый объект-мусор каждый кадр void Update() { var data = new TemporaryData(); // Аллокация в Gen0 Process(data); } // ЛУЧШЕ: Переиспользование объекта private TemporaryData _cachedData = new TemporaryData(); void Update() { _cachedData.Reset(); Process(_cachedData); // Нет аллокации } - Gen2 и "большие" фризы. Объекты, подолгу остающиеся в памяти (кеши, глобальные менеджеры), со временем попадают в Gen2. Их массовое освобождение вызовет Full GC — главную причину длительных пауз в работе игры. Важно управлять их жизненным циклом.
- Структуры (struct) vs Классы (class). Значимые типы (
struct), хранящиеся в стеке или внутри других объектов, не контролируются GC и не создают нагрузки на него, что делает их предпочтительными для небольших, короткоживущих данных (например,Vector3,RaycastHit).
Вывод: Понимание поколений помогает писать код, минимизирующий нагрузку на GC. Основная стратегия: сокращать частые аллокации в Gen0 (через пулы объектов, кэширование) и контролировать накопление объектов в Gen2, чтобы избегать разрушительных по производительности Full GC-сборок. Мониторинг через Unity Profiler (окно Memory > GC Alloc) является обязательной практикой для оптимизации.