Урок по треку C# и .NET 8 мин

GC, IDisposable и производительность

Поколения, LOH, финализаторы, Dispose-паттерн, Span, stackalloc, ArrayPool.

Управляемая память не означает «о памяти можно не думать». На middle и senior собесах спрашивают устройство GC, разницу Dispose и финализатора, утечки и способы снизить аллокации.

Как работает GC

Сборщик мусора трассирующий: от корней (локальные переменные в стеках, статические поля, закреплённые дескрипторы) он помечает всё достижимое, остальное мусор. Циклические ссылки не мешают: недостижимый цикл соберётся.

После пометки живые объекты сдвигаются (уплотнение), ссылки обновляются. Поэтому выделение памяти в .NET дешёвое: это сдвиг указателя.

Поколения

  • Gen 0: новые объекты, сборка частая и быстрая.
  • Gen 1: буфер между молодыми и старыми.
  • Gen 2: долгоживущие объекты, полная сборка редкая и дорогая.

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

LOH: объекты от 85 000 байт. Собирается вместе с Gen 2 и по умолчанию не уплотняется, отсюда фрагментация. POH (.NET 5): куча для закреплённых объектов.

Режимы: Workstation и Server GC (в ASP.NET Core по умолчанию Server: куча и поток сборки на ядро). Фоновая сборка Gen 2 работает параллельно с приложением.

IDisposable

GC освобождает память, но не знает, когда закрыть файл, сокет или соединение. Для этого IDisposable и using:

await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();

Реализуйте IDisposable, если класс владеет неуправляемым ресурсом или другими IDisposable. Шаблон:

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
    if (_disposed) return;
    if (disposing) _stream.Dispose();   // управляемые ресурсы
    // неуправляемые ресурсы освобождаются в обоих случаях
    _disposed = true;
}

Финализатор

~MyClass() вызывается потоком финализации в неизвестный момент. Объект с финализатором переживает лишнюю сборку: сначала попадает в очередь, потом освобождается. Финализатор нужен только как страховка для неуправляемых ресурсов, и лучше обернуть ресурс в SafeHandle. GC.SuppressFinalize в Dispose снимает объект с финализации.

Утечки в .NET

GC не соберёт то, что достижимо от корня:

  • статические коллекции-кеши без вытеснения;
  • подписки на события долгоживущих объектов;
  • таймеры, которые держат колбэк;
  • захват больших объектов в замыканиях;
  • неосвобождённые неуправляемые ресурсы.

Диагностика: dotnet-counters (размер поколений, частота сборок), dotnet-gcdump и dotnet-dump (кто держит объекты).

Span<T> и Memory<T>

Span<T> это окно на непрерывную память: массив, строку, стек или неуправляемый буфер. Срезы без копирования, но это ref struct: нельзя в поле класса, в лямбду, через await.

Span<byte> buffer = stackalloc byte[256];          // маленький буфер на стеке
ReadOnlySpan<char> part = text.AsSpan(5, 3);
int n = int.Parse(part);

Memory<T> можно хранить и передавать в async-методы, а .Span брать по месту.

Пулы

ArrayPool<T>.Shared.Rent(size) возвращает массив не меньше нужного, обязательно Return, данные внутри не очищены. ObjectPool<T> для дорогих объектов вроде StringBuilder.

Как снижать аллокации

  1. Измерить: BenchmarkDotNet с MemoryDiagnoser, профилировщик.
  2. Убрать лишние строки: span-парсинг, StringBuilder, ёмкость заранее.
  3. Убрать упаковку и замыкания в горячих циклах.
  4. Большие временные буферы брать из пула, чтобы не трогать LOH.
  5. Маленькие неизменяемые значения делать readonly struct.

Частые ошибки

  • Вызывать GC.Collect() «для надёжности».
  • Писать финализатор без неуправляемых ресурсов.
  • Забывать Dispose у HttpResponseMessage, потоков, CancellationTokenSource.
  • Считать, что циклические ссылки приводят к утечке.

Что спросят на собесе

  • Как работает GC и зачем поколения?
  • Что такое LOH и чем она опасна?
  • Чем Dispose отличается от финализатора, зачем SuppressFinalize?
  • Какие бывают утечки памяти в .NET и как их искать?
  • Что такое Span<T> и почему его нельзя хранить в поле класса?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.