Сборщик мусора: поколения, LOH, режимы GC
↑ 2.2 .NET runtime: CLR, память, GC · ← Предыдущая · → Следующая
Зачем это на собесе
GC — главный вопрос про .NET runtime на middle+/senior собесе: поколения, LOH, режимы Workstation/Server, паузы, влияние на латентность API и память контейнера.
Объяснение
Как работает GC
.NET использует трассирующий сборщик с поколениями и уплотнением (compacting):
- Mark — от корней (стеки потоков, статические поля, регистры, GC handles) помечаются достижимые объекты.
- Sweep/Plan — определяются мёртвые объекты.
- Compact — живые объекты сдвигаются, чтобы убрать фрагментацию, ссылки обновляются. Выделение памяти очень дешёвое: указатель просто сдвигается в текущем сегменте (bump allocation).
Поколения
Гипотеза: большинство объектов умирают молодыми.
- Gen 0 — новые объекты. Собирается чаще всего и быстро (мало живых).
- Gen 1 — пережившие одну сборку, буфер между молодыми и старыми.
- Gen 2 — долгоживущие (синглтоны, кэши, статические данные). Полная сборка дорогая. Выжившие при сборке объекты продвигаются в следующее поколение. Сборка Gen N включает все младшие поколения.
LOH и POH
- Large Object Heap — объекты ≥ 85 000 байт (большие массивы, строки,
MemoryStreamс большими буферами). Логически относится к Gen 2, по умолчанию не уплотняется → фрагментация. Можно уплотнить разово:GCSettings.LargeObjectHeapCompactionMode = CompactOnce. - Pinned Object Heap (.NET 5+) — для закреплённых объектов (буферы для I/O), чтобы они не мешали уплотнению.
Режимы GC
- Workstation — одна куча, оптимизирован под отзывчивость UI. По умолчанию для консольных и десктопных приложений.
- Server — отдельная куча и поток GC на каждое ядро, выше пропускная способность, больше памяти. По умолчанию для ASP.NET Core.
- Concurrent/Background GC — сборка Gen 2 идёт параллельно с работой приложения, паузы короче.
- DATAS (Dynamic Adaptation To Application Sizes, по умолчанию с .NET 9 для Server GC) — динамически подстраивает число куч под нагрузку: меньше памяти в контейнерах.
<PropertyGroup>
<ServerGarbageCollector>true</ServerGarbageCollector>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>Или переменные: DOTNET_gcServer=1, DOTNET_GCHeapHardLimit, DOTNET_GCHeapHardLimitPercent=75.
Паузы
Во время части фаз GC потоки приложения останавливаются (stop-the-world). Для API это рост p99 латентности. Паузы тем длиннее, чем больше живых объектов и чем больше Gen 2.
GC в контейнерах
- .NET учитывает cgroup-лимиты: лимит кучи по умолчанию ~75% лимита памяти контейнера.
- Server GC на машине с многими ядрами, но с небольшим лимитом памяти может резервировать много: DATAS или Workstation для маленьких сервисов.
Как снизить нагрузку на GC
- Меньше аллокаций в горячих путях: Span, ArrayPool, struct для маленьких значений, StringBuilder, избегать боксинга и LINQ в самых горячих циклах.
- Не создавать большие временные массивы (LOH): потоковая обработка, пулы (
RecyclableMemoryStream). - Не держать ненужные ссылки в долгоживущих объектах (кэши без ограничения размера, статические списки).
GC.Collect()вызывать не нужно: почти всегда вредно.
Диагностика
dotnet-counters monitor -p <pid> --counters System.Runtime
# gc-heap-size, gen-0/1/2-gc-count, time-in-gc, alloc-rate, loh-size
dotnet-gcdump collect -p <pid> # снимок кучи для анализа в VS или PerfViewВ коде: GC.GetGCMemoryInfo(), GC.CollectionCount(0), метрики System.Runtime через OpenTelemetry.
Нюансы и подводные камни
- Высокий
% time in GC(больше 10–20%) и частые Gen 2 — признак проблем с аллокациями или утечкой. GC.Collect()в коде продвигает объекты в старшие поколения и ухудшает ситуацию.- Финализаторы откладывают освобождение объектов на одну сборку и нагружают поток финализации.
- Много закреплённых (pinned) объектов мешает уплотнению.
Вопросы с ответами
Как работает сборщик мусора в .NET?
Трассирующий, с поколениями и уплотнением: от корней помечаются достижимые объекты, мёртвые освобождаются, живые сдвигаются для устранения фрагментации. Молодые поколения собираются часто и быстро, старшие — редко.
Что такое поколения и зачем они?
Gen 0, 1, 2 основаны на гипотезе, что большинство объектов живут недолго. Частая сборка только молодых объектов дешевле полной сборки всей кучи.
Что такое LOH и чем он опасен?
Куча для объектов от 85 000 байт. Относится к Gen 2 и по умолчанию не уплотняется, поэтому фрагментируется. Частое создание больших временных массивов ведёт к росту памяти и полным сборкам.
Чем Server GC отличается от Workstation GC?
Server GC использует отдельную кучу и поток на каждое ядро: выше пропускная способность, но больше памяти. Workstation — одна куча, меньше памяти и акцент на отзывчивость. ASP.NET Core по умолчанию использует Server GC.
Почему не стоит вызывать GC.Collect вручную?
GC сам лучше знает, когда собирать. Ручной вызов вызывает паузу, продвигает живые объекты в старшие поколения и обычно ухудшает производительность.
Связанные темы
- Предыдущая: Стек и куча, boxing и unboxing · Следующая: IDisposable, using, финализаторы, IAsyncDisposable
- Утечки памяти в .NET: Утечки памяти в .NET и как их искать
- Пулы объектов: Пулы объектов: ArrayPool, ObjectPool, RecyclableMemoryStream
- Профилирование: Профилирование: dotnet-counters, dotnet-trace, dotnet-dump, PerfView
- GC в JavaScript: Память и сборщик мусора, утечки