Garbage Collector (GC) — одного из самых важных «невидимых помощников» в.NET.
Он избавляет нас от ручного управления памятью, но как именно?
Что такое Garbage Collection?
GC — это автоматический механизм.NET, который освобождает память от объектов, которые больше не используются. Если бездумно создавать переменные, то приложение будет занимать все больше и больше места в памяти, и тем самым память полностью закончится. Этой памятью нужно как‑то управлять, и этой задачей занимается GC. Его единственная задача, удалять неиспользуемые переменные, которые ты создаешь по ходу выполнения приложения. Благодаря нему не приходится в ручную очищать неиспользуемые переменные. Без него приложение быстро бы «съело» всю доступную RAM, создавая новые переменные и не очищая старые.
Где живут переменные?
В .NET переменные хранятся в разных областях памяти, в зависимости от их типа и времени жизни. Основные места:
В стеке (Stack) хранятся значимые типы, например: int, byte, long, decimal, bool, char, enum, struct.
В куче (Heap) хранятся ссылочные типы, например: object, string, class, interface, delegate, а указатель на объект в куче хранится в стеке.
У каждого потока — свой стек, у процесса — общая куча.
Рассмотрим каждую из них подробно.
1. Стек (Stack)
Что хранится:
Локальные переменные методов (значимые типы —
int
,float
,struct
и т. д.).Аргументы методов.
Указатели на объекты в куче (для ссылочных типов).
Особенности:
Автоматическое управление: память выделяется при входе в метод и освобождается при выходе.
Быстрый доступ: операции со стеком очень быстрые (просто сдвиг указателя).
Ограниченный размер: если стек переполняется (например, из-за глубокой рекурсии), возникает
StackOverflowException
.
2. Управляемая куча (Managed Heap)
Что хранится:
Объекты ссылочных типов (
class
,string
, массивы и т. д.)."Боксированные" значимые типы (когда
struct
приводится кobject
).
Особенности:
Динамическое выделение памяти: объекты создаются оператором
new
.Управление GC: память освобождается автоматически сборщиком мусора.
Фрагментация и компактификация: GC может перемещать объекты для уменьшения фрагментации.
Подробнее про управляемую кучу:
-
Поколения (Generations):
Gen 0: молодые объекты (часто удаляются).
Gen 1: "промежуточные" объекты.
Gen 2: долгоживущие объекты (например, статические поля).
LOH (Large Object Heap): большие объекты (> 85 КБ).
3. Неуправляемая куча (Unmanaged Heap)
Что хранится:
Ресурсы, выделенные вне .NET (через P/Invoke, COM,
Marshal.AllocHGlobal
).Дескрипторы файлов, сокетов, графические ресурсы.
Особенности:
Нет управления GC: память надо освобождать вручную (
Dispose()
,Close()
).Опасность утечек: если забыть освободить, память будет занята до завершения процесса.
Особые случаи
Строки (string)
Хранятся в куче, но из-за иммутабельности и интернирования могут иметь неочевидное поведение.
Литералы (
"Hello"
) могут попадать в пул интернированных строк.
Очистку GC можно вызвать в ручную (хоть это и не рекомендуется) при помощи методаGc.Collect();
Примеры ситуаций, когда нужно явно вызвать очистку:
перед началом исполнения кода, который не должен быть прерван GC во время исполнения;
после создания большого количества объектов в приложении;
Устройство поколений в GC.
Для чего нужно разделение в куче на три поколения?
Поколения (Generations) — это ключевой механизм в работе сборщика мусора (GC) .NET, который оптимизирует процесс очистки памяти, уменьшая паузы и повышая производительность.
Простыми словами, поколения нужны для оптимизации, что бы каждый раз не перебирать всю кучу и не тратить на это все ресурсы.
Разделение памяти кучи на три поколения (Generation 0, 1, 2) в сборщике мусора (Garbage Collector, GC) помогает оптимизировать очистку памяти.
Garbage Collector (GC) в .NET работает по поколенческой (generational) модели и использует три поколения, нумеруемые от 0.
Гипотеза поколений (Generational Hypothesis) — это основа работы GC в .NET. Она гласит:
Большинство объектов живут недолго (например, временные переменные в методах).
Чем дольше живёт объект, тем выше вероятность, что он проживёт ещё дольше.
Generation 0 (Первое поколение) – Очистка происходит когда выделено определенное количество памяти или когда в системе недостаточно памяти.
• В этом поколении создаются новые объекты.
• Сборки происходят часто, но занимают мало времени.
• Если объект переживает сборку мусора, он перемещается в Gen 1.Generation 1 (Второе поколение) – Очистка происходит когда Gen 0 заполняется, и GC не смог освободить достаточно памяти.
Является Промежуточным поколением для объектов, которые пережили первую сборку мусора.
• Содержит объекты, которые выжили после очистки Gen 0.
• Используется как буфер перед Gen 2.
• Очистка Gen 1 происходит реже, чем Gen 0.Generation 2 (Третье поколение) – Очистка происходит, когда в системе не хватает памяти.
Для долгоживущих объектов, которые редко удаляются (например, кеши, синглтоны).
• Включает долгоживущие объекты, например, кеш, синглтоны.
• Очистка происходит редко и занимает больше времени.
• Все объекты, пережившие сборку в Gen 1, перемещаются в Gen 2.
Как влияет на производительность?
Частые сборки Gen 0 — быстрые, почти незаметные.
Сборки Gen 2 — могут вызывать заметные паузы (особенно если куча большая).
Фрагментация LOH — может приводить к
OutOfMemoryException
, даже если свободная память есть.
Оптимизации:
Избегать лишних аллокаций (например, переиспользовать буферы).
Использовать
struct
для короткоживущих данных (они не попадают в кучу).Настройка режима GC (
Server
vsWorkstation
, фоновый GC).
Чем меньше объектов доживает до Gen 2, тем лучше работает GC.
Этапы сборки мусора в GC
Приостановка потоков. (STW — Stop-The-World)
Маркировка. (Marking)
Удаление недостижимых объектов. (Sweeping)
Сжатие. (Compaction, опционально)
Выполнение всех финализаторов.
Возобновление потоков.
1. Приостановка потоков (Stop-The-World)
Что происходит:
Все управляемые потоки приложения приостанавливаются (кроме потока, выполняющего сборку).
Это нужно, чтобы граф объектов не менялся во время анализа.
Почему это важно?
Без остановки потоков новые ссылки могут появляться/исчезать, и GC не сможет корректно определить, какие объекты действительно "мертвы".
2. Маркировка (Marking)
Что происходит:
GC сканирует корневые ссылки (Roots), чтобы найти все живые объекты.
-
Корни включают:
Локальные переменные в стеке (Stack)
Статические поля
Управляемые указатели в регистрах CPU
Ссылки из финализаторов
GC рекурсивно обходит все ссылки из корней и помечает объекты как "достижимые".
? Важно:
Объекты, не помеченные как достижимые, считаются мусором.
Если объект в Gen 2 ссылается на объект в Gen 0, последний не будет удалён (потому что Gen 2 — корень).
3. Удаление недостижимых объектов (Sweeping)
Что происходит:
GC освобождает память, занимаемую недостижимыми объектами.
В некомпактифицируемых кучах (например, LOH до .NET Core 3.0) память просто помечается как свободная.
В Gen 0/1/2 (если включена компактификация) объекты перемещаются, чтобы уменьшить фрагментацию.
Особенности:
Мелкие объекты (Gen 0/1) обычно удаляются быстро.
Крупные объекты (LOH) могут оставлять "дыры" (фрагментацию).
4. Компактификация кучи (Compaction, опционально)
Что происходит:
-
Выжившие объекты перемещаются в начало кучи, чтобы:
Уменьшить фрагментацию.
Ускорить выделение новых объектов (новые аллокации идут последовательно).
Обновляются все ссылки на перемещённые объекты.
5. Выполнение всех финализаторов(Finalization)
Что происходит:
Если у объекта есть финализатор (
~ClassName
), он не удаляется сразу.Вместо этого он попадает в очередь финализации (
freachable
queue).Поток финализатора (
Finalizer thread
) постепенно вызываетFinalize()
у таких объектов.Только после этого память освобождается.
? Проблемы финализаторов:
Замедляют сборку мусора (объект живёт дольше).
Могут не выполниться при аварийном завершении.
✅ Решение:
Использовать
IDisposable
+using
вместо финализаторов.
6. Возобновление потоков
Что происходит:
Все приостановленные потоки возобновляют работу.
Если была компактификация, ссылки в них уже обновлены.
В .NET управляемая куча (managed heap) разделена на несколько областей, каждая из которых оптимизирована для определенных типов объектов.
Рассмотрим основные из них:
SOH (Small Object Heap) - Куча малых объектов
до 85,000 байт (для примитивных типов и массивов, кроме double[]);
разделена на три поколения (Gen 0, Gen 1, Gen 2);
подвергается компактификации (дефрагментации) при сборке мусора;
быстрый доступ к объектам;
Дефрагментация - это процесс, при котором выжившие объекты перемещаются в один непрерывный блок памяти, чтобы освободить место для новых объектов. Простыми словами, когда объект удаляется, что бы не было пустых промежутков между ячейками памяти, они перемещаются ближе друг к другу (сжимаются).
LOH (Large Object Heap) - это куча для больших объектов
более 85,000 байт (или ≥ 1,000 элементов для double[]);
в этой куче память всегда фрагментирована (отсутсвует сжатие памяти);
сборка происходит только при Gen 2 collection;
может вызывать значительные паузы в работе приложения;
чистить все эти большие объекты дорого, поэтому LOH начинается только тогда, когда начинается сборка в SOH во втором поколении (3 по счету);
POH Pinned Object Heap (.NET 5) - куча закрепленных объектов (те которые имеют фиксированный аддрес в памяти).
уменьшает фрагментацию основной кучи;
оптимизирована для работы с межоперабельными вызовами (interop);
объекты не перемещаются GC;
FOH - (Frozen Object Heap) (.NET 8) - объекты, которые живут все время приложения, и не ссылаются на другие объекты не из FOH.
оптимизация для разгрузки GC, что бы он еще раз эти объекты не проверял;
специальная область для статических данных рантайма;
не доступна напрямую для разработчиков;
содержит неизменяемые системные объекты;
ускоряет запуск приложения;
Типы сборщиков мусора в .NET. В .NET доступны два типа GC
-
Workstation GC - оптимизирован для клиентских приложений. Имеет паралельный режим с минимальной задержкой:
-
Workstation GC with Concurrent
позволяет продолжать выполнение пользовательского кода во время фоновой сборки;
по умолчанию для клиентских приложений;
снижает "замирания" UI (но не устраняет полностью);
использует дополнительный фоновый поток для сборки Gen 2.
подходит для интерактивных приложений (например, редакторов, игр);
-
Workstation GC without Concurrent
полностью блокирует выполнение кода на время сборки мусора (всех поколений);
может вызывать заметные "фризы" UI;
чуть выше пропускная способность (throughput), чем у concurrent-режима;
используется редко — только для специфических сценариев;
-
-
Server Garbage Collector (GC) - оптимизирован для серверных приложений (включая ASP.NET Core)
создает отдельный GC thread для каждого логического процессора;
параллельная обработка куч (нагрузка распределяется между ядрами CPU);
быстрее расширяет кучу при нехватке памяти;
реже вызывает сборки, но они более масштабные;
Выбор типа GC сильно влияет на производительность, поэтому тестируйте под нагрузкой!
Финализация
Метод Finalize() представляет собой специальный метод, который предназначен для выполнения финальной очистки ресурсов перед тем, как объект будет уничтожен сборщиком мусора. Это метод, который может быть определен в классе для очистки неуправляемых ресурсов, если класс не реализует интерфейс IDisposable.
Другими словами, финализаторы — это специальные методы, которые выполняются перед уничтожением объекта сборщиком мусора (GC). Они представляют собой механизм для освобождения неуправляемых ресурсов, когда разработчик явно не вызвал метод Dispose(); (если неуправляемые ресурсы перед удалением не освободить, то удаление выполнить не получится, так как будет ошибка по типу этот файл занят другим процессом)
Неуправляемые и управляемые ресурсы:
Неуправляемые ресурсы включают в себя ресурсы, которые не управляются средой CLR (Common Language Runtime), например, файловые дескрипторы, сетевые соединения или указатели на память, выделенную вне .NET среды.
Управляемые ресурсы - это объекты .NET, которые занимают память и потенциально удерживают ссылки на неуправляемые ресурсы.
Особенности метода:
Автоматический вызов: Сборщик мусора автоматически вызывает Finalize() на объекте, который не имеет других активных ссылок и который определяет финализатор. Это происходит непосредственно перед тем, как сборщик мусора освобождает память, занимаемую объектом.
Определение в базовом классе
Object
: Все объекты наследуют от базового класса Object, который предоставляет реализацию Finalize(). Однако в большинстве случаев Finalize() не имеет реализации и не делает ничего, пока не будет переопределен в производном классе.Замедление сборки мусора: Наличие объектов с финализаторами может замедлить процесс сборки мусора, так как объекты, требующие финализации, должны быть обработаны дважды: сначала они помещаются в очередь финализации, а затем их память освобождается после выполнения Finalize().
Жизненный цикл объекта:
Создание объекта и регистрация финализатора Когда создается новый объект, он размещается в управляемой куче (managed heap). Если у объекта есть финализатор (деструктор ~ClassName()), то ссылка на этот финализатор добавляется в список на финализацию.
Использование объекта и освобождение ресурсов При работе с неуправляемыми ресурсами (например, HttpClient, подключения к БД, файловые потоки) важно явно освобождать их, вызывая:
Dispose() (если объект реализует IDisposable),
или используя блок using. Если этого не сделать, сборщик мусора (GC) не сможет сразу освободить ресурсы, и тогда в дело вступает финализатор.
-
Обнаружение мусора и перемещение финализатора в очередь Когда GC запускает сборку мусора, он:
Проверяет, достижим ли объект (есть ли на него ссылки).
-
Если объект недостижим, но у него есть финализатор, то:
сам объект не удаляется;
его финализатор перемещается из списка на финализацию в очередь финализации;
Объект с финализатором переживает первую сборку мусора! Это значит, что вместо удаления он переходит в следующее поколение (Gen 1 или Gen 2), что увеличивает время его жизни.
Выполнение финализатора Очередь финализации — это отдельный список финализаторов, которые должны быть выполнены. После завершения основной работы GC, специальный поток финализатора (Finalizer Thread) начинает:
Извлекать финализаторы из очереди.
Выполнять их перед окончательным удалением объекта.
Рекомендации:
Избегайте финализаторов, когда это возможно: Предпочтительнее использовать паттерн IDisposable, чтобы явно освобождать ресурсы.
Используйте GC.SuppressFinalize(): В классах, реализующих IDisposable, вызовите GC.SuppressFinalize(this), чтобы предотвратить вызов финализатора и ускорить освобождение памяти.
Освобождайте ресурсы в финализаторе только если это абсолютно необходимо: Так как выполнение финализатора непредсказуемо, старайтесь освобождать ресурсы в методе Dispose().
Как определить его:
class SampleClass
{
// Конструктор
public SampleClass() {
// Инициализация ресурсов
}
// Финализатор
~SampleClass() {
// Код очистки ресурсов
}
}
При уничтожении объекта сборщик мусора вызовет финализатор автоматически. Это важный элемент управления ресурсами, но его использование должно быть ограничено из-за потенциальных негативных последствий для производительности приложения.
Dispose
Dispose метод является частью паттерна управления ресурсами, известного как "Dispose Pattern". Этот метод реализуется в классах через интерфейс IDisposable.
Цель — явное освобождение неуправляемых ресурсов и, по желанию, управляемых ресурсов, прежде чем сборщик мусора освободит объект. Это важно для эффективного управления памятью и другими системными ресурсами.
Интерфейс IDisposable
содержит всего один метод — Dispose()
. Основное назначение этого метода — освобождение неуправляемых ресурсов, таких как файловые потоки, сетевые подключения или дескрипторы операционной системы.
Интересно, что финализаторы (деструкторы) в классах служат ровно той же цели — очистке неуправляемых ресурсов.
Однако между этими подходами есть принципиальная разница в механизме работы:
Финализатор выполняется сборщиком мусора в неопределённый момент времени после того, как объект стал недостижимым. Это создаёт две проблемы:
Ресурсы могут освобождаться с существенной задержкой
Момент очистки непредсказуем, что может привести к неэффективному использованию ресурсов
IDisposable
решает эти проблемы, предоставляя разработчику контроль над процессом освобождения ресурсов. Вместо того чтобы полагаться на GC, программист сам определяет оптимальный момент для вызова Dispose()
, обычно используя конструкцию using
.
Ключевые преимущества IDisposable
:
Детерминированное освобождение ресурсов
Возможность немедленного закрытия файлов/соединений
Предотвращение утечек ресурсов
Отсутствие дополнительной нагрузки на сборщик мусора
Финализаторы стоит рассматривать лишь как резервный механизм на случай, если разработчик забудет вызвать Dispose()
. Основную логику очистки всегда следует реализовывать через IDisposable
.
Как он работает?
Должен освобождать все неуправляемые ресурсы, занимаемые объектом, а также должен иметь возможность освобождать управляемые ресурсы, если это необходимо. Как правило, управляемые ресурсы освобождаются сами сборщиком мусора, но если управляемый ресурс включает в себя неуправляемые ресурсы, тогда Dispose может быть вызван для их явного освобождения.
Пример:
class MyClass : IDisposable {
public void Dispose() {
// Очистка ресурсов
}
}
Но этот пример не является правильным. Правильнее всего использовать using(), как в следующем примере:
using (...)
{
// Использование ресурса
}
// Метод Dispose автоматически вызывается при выходе из блока using
Под капотом using() конвертируется в try{} finaly{}. В try будет все, что находится в области using(), а в finaly автоматически будет выполнен Dispose();
Финализатор или IDisposable:
разработчик не занимается управлением финализатора, а за IDisposable отвечает сам
Финализатор будет точно вызван, но не понятно когда. А IDisposable будет вызван вручную, и очищать память будет так же в ручную.
если вызван Dispose, то финализатор вызывать не нужно
Заключение: Работа с Garbage Collector в .NET
Garbage Collector (GC) — автоматический механизм управления памятью в .NET. Он освобождает память от неиспользуемых объектов, предотвращая утечки.
1. Основные понятия
Стек (Stack)
Хранит значимые типы (
int
,bool
,struct
).Быстрый доступ, автоматическое освобождение при выходе из метода.
Куча (Heap)
Хранит ссылочные типы (
object
,string
,class
).Управляется GC, требует сборки мусора.
Поколения (Generations)
Оптимизация GC для эффективной очистки:
Gen 0 — новые объекты, сборка частая (быстрая).
Gen 1 — объекты, пережившие одну сборку Gen 0.
Gen 2 — долгоживущие объекты (статические поля, кеши).
2. Типы куч:
SOH (Small Object Heap) — объекты < 85 КБ, дефрагментируется.
LOH (Large Object Heap) — объекты ≥ 85 КБ, без сжатия.
POH (Pinned Object Heap) — закрепленные объекты (.NET 5+).
FOH (Frozen Object Heap) — неизменяемые объекты (.NET 8+).
3. Типы сборщиков
Workstation GC (клиентские приложения)
Concurrent — фоновая сборка (меньше лагов, для UI).
Non-Concurrent — полная остановка (выше throughput).
Server GC (высоконагруженные серверы)
Параллельная сборка на всех ядрах CPU.
Большие кучи, выше потребление памяти.
4. Финализация и Dispose:
Финализатор (~ClassName) — вызывается GC (непредсказуемо).
IDisposable — явное освобождение ресурсов через Dispose().
Рекомендации:
Избегайте финализаторов, используйте IDisposable.
Вызывайте GC.SuppressFinalize(this) в Dispose().
Хотя GC автоматически управляет памятью, для достижения максимальной производительности важно учитывать три ключевых аспекта:
1. Работа с неуправляемыми ресурсами
2. Выбор типа GC
3. Оптимизация работы с поколениями
Комментарии (4)
fornit1917
18.06.2025 07:39Простыми словами, поколения нужны для оптимизации, что бы каждый раз не перебирать всю кучу и не тратить на это все ресурсы.
Это не совсем верно. Для определения недостижимых объектов куча всё равно "перебирается" вся целиком, т.к. осуществляется построение и обход графа всех объектов независимо от поколений.
А вот последующие этапы сборки мусора (освобождение памяти, дефрагментация) уже оптимизируются за счёт механизм поколений т.к. выполняются не на всей куче, а на поколении.
Nagg
18.06.2025 07:39Очень сомнительная поправка про "всю кучу", в большинстве случаев ГЦ сканит конкретно одно поколение + области памяти в которых могут быть ссылки на первое поколение (это когда write barrier оставляет в записи в card table каждый раз когда вы любой объект присваиваете в поле другого на хипе) + руты где бы они не были
RodionGork
всё же лучше быть аккуратнее с подобными "популистскими" фразами в заголовках, а то так можно и до статьи "C# - зачем он нужен" дойти со временем :)