1/31
Looks like no tags are added yet.
Name | Mastery | Learn | Test | Matching | Spaced | Call with Kai |
|---|
No analytics yet
Send a link to your students to track their progress
Когда триггерится сборка мусора?
Основной сценарий: при попытке выделить память в Gen 0, когда там нет свободного места
Другие триггеры:
- При выделении в LOH, если превышен порог
- При нехватке памяти в системе (memory pressure)
- По таймеру в фоновом GC
- Явный вызов GC.Collect()
Пример кода для явного вызова:
GC.Collect(); // собирает все поколения
GC.Collect(0); // только Gen 0
GC.Collect(2, GCCollectionMode.Forced); // принудительно Gen 2
Какие есть фазы сборки мусора? В каком порядке они происходят?
Mark — пометка живых объектов
Sweep — удаление мертвых объектов
Compact — уплотнение кучи (перемещение объектов)
Что происходит во время фазы Mark? Как GC помечает объекты?
GC проходит граф объектов от GC Roots, помечая каждый достижимый объект специальным битом
Как это работает:
- У каждого объекта в куче есть заголовок (object header)
- В заголовке есть специальный бит для пометки GC
- GC устанавливает этот бит в 1 для живых объектов
- Если объект уже помечен — повторно не обрабатывается (избегание циклов)
Заголовок объекта содержит:
- Sync block index (для lock и других целей)
- Method table pointer (информация о типе)
- Флаги, включая бит пометки GC
Что происходит во время фазы Sweep?
GC проходит по тем поколениям, которые мы собираем и освобождает память от объектов, которые НЕ помечены как живые. В куче остаются только живые объекты, но между ними образуются дыры
Что происходит во время фазы Compact? Что со ссылками?
GC перемещает все живые объекты к началу кучи, убирая фрагментацию. GC автоматически обновляет ВСЕ ссылки на перемещенные объекты. Мы никогда не получим битую ссылку после GC
Что такое GC Roots? Что является GC Roots?
Точки входа для поиска живых объектов:
- Локальные переменные и параметры методов в стеке
- Статические поля классов
- Регистры процессора
- Pinned объекты
- Handles в P/Invoke сценариях
- Объекты в финализационной очереди
- Объекты, на которые ссылаются из finalizer-reachable очереди
Какие объекты считаются мусором?
Недостижимые объекты — те, до которых невозможно добраться ни по одному пути от любого GC Root
Аналогия: представь сундук с объектами, где некоторые объекты связаны между собой лесками. Из сундука торчат концы некоторых лесок — это GC Roots. Если потянуть за любую торчащую леску и по цепочке лесок можно достать объект (напрямую или через другие объекты), то он достижимый. Объекты, которые никак не связаны с торчащими лесками — мусор
Объект должен быть достижим от GC Roots — точек, которые гарантированно живые (локальные переменные активных методов, статические поля и т.д.)
Зачем нужны поколения в GC?
Оптимизация на основе гипотезы поколений: молодые объекты умирают быстрее старых. 85% объектов убирают в поколении 0.
Сборка Gen 0 происходит быстро (мало объектов). Долгоживущие объекты в Gen 2 проверяются реже
При сборке поколения X какие поколения сканируются?
Gen 0: сканируется только Gen 0
Gen 1: сканируются Gen 0 + Gen 1
Gen 2: сканируются Gen 0 + Gen 1 + Gen 2 (full GC)
Если на объект из Gen 0 ссылается только объект из Gen 2, соберет ли его GC?
Нет. При сборке в Gen 0 и Gen 1 проверяются ссылки из старших поколений через card table
Что такое LOH и какие объекты туда попадают?
Large Object Heap — отдельная куча для объектов размером ≥85000 байт (размер можно настроить)
Что попадает:
- Большие массивы
- Крупные строки
- Большие объекты
Особенности:
- Сразу считается Gen 2
- Долго не было дефрагментации (сейчас есть опционально)
Пример:
byte[] bigArray = new byte[100_000]; // попадет в LOH
string longString = new string('x', 50_000); // тоже в LOH
Почему большие объекты не хранятся в обычной куче?
Перемещение больших объектов во время Compact фазы слишком дорого по времени
Компромисс: Скорость vs фрагментация
Есть ли Compact у LOH?
.NET Framework: НЕТ (только Mark & Sweep)
.NET Core/.NET 5+: ДА, но по требованию через настройки
Включение дефрагментации:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); // следующая полная сборка выполнит дефрагментацию LOH
Что такое "Stop the World"?
Приостановка всех потоков приложения во время критических фаз сборки мусора для обеспечения целостности кучи
В чем разница между Server и Workstation GC?
Workstation GC:
- Одна общая куча для всех потоков
- Оптимизирован для минимальных пауз (UI отзывчивость)
- Concurrent GC по умолчанию
- Меньше потребляет памяти
- Сборка на том же потоке, что выделял память
Server GC:
- Отдельный сегмент кучи для каждого логического процессора
- Объекты могут ссылаться между сегментами
- Выделение памяти происходит в "своем" сегменте потока
- Параллельная сборка несколькими потоками
- Оптимизирован для пропускной способности
- Background GC для Gen 2
- Больше потребляет памяти
Важно: это всё одна управляемая куча, просто разделенная на сегменты для параллельной работы. Объект из сегмента CPU 0 может свободно ссылаться на объект из сегмента CPU 1
Настройка:
Зачем нужен Pinned Object Heap (POH)?
Специальная куча для закрепленных (pinned) объектов, которые нельзя перемещать в памяти
Что такое пиннинг: когда мы передаем объект в неуправляемый код (например, Windows API или C++ библиотеку), мы даем ему адрес объекта в памяти. GC не может переместить такой объект, потому что не сможет обновить этот адрес в неуправляемом коде
Аналогия: это как дать кому-то карту с местом клада. Если мы переместим клад, но не можем обновить карту — человек придет на старое место и ничего не найдет
До POH: pinned объекты создавали дыры в обычной куче (фрагментация)
С POH: такие объекты сразу попадают в специальную кучу
Пример использования:
byte[] buffer = GC.AllocateArray
// buffer в POH, можно безопасно передавать в native код
Зачем нужен финализатор? Какой код туда помещать?
Для освобождения неуправляемых ресурсов (файловые дескрипторы, нативная память, хэндлы Windows)
Пример кода с финализатором:
class FileWrapper
{
private IntPtr fileHandle;
~FileWrapper()
{
if (fileHandle != IntPtr.Zero)
{
NativeMethods.CloseHandle(fileHandle);
}
}
}
Когда вызывается финализатор?
После того, как объект определен как мусор, но перед его удалением
1. Объект становится недостижимым
2. GC помещает объект в очередь финализации
3. Отдельный поток выполняет финализаторы
4. При следующей сборке объект удаляется
Важно: Финализатор продлевает жизнь объекта минимум на одну сборку!
На каком потоке и с какими гарантиями выполняется финализатор?
Специальный отдельный поток финализации
Гарантии:
- НЕТ гарантий порядка выполнения
- НЕТ гарантий времени выполнения
- НЕТ гарантий выполнения при завершении приложения
- Если финализатор завис, он блокирует выполнение всех остальных
- Если упал с исключением, исключение игнорируется
Как убрать объект из очереди финализации?
GC.SuppressFinalize(this) — обычно вызывается в методе Dispose()
public void Dispose()
{
// Освобождаем ресурсы
CleanupResources();
// Говорим GC не вызывать финализатор
GC.SuppressFinalize(this);
}
Как влияет наличие финализатора на срок жизни объекта?
Финализатор увеличивает время жизни объекта:
1. Объект переживает первую сборку мусора
2. Помещается в очередь финализации
3. Переводится в следующее поколение
4. Удаляется только при следующей сборке этого поколения
Пример влияния:
- Объект без финализатора в Gen 0: удален при первой же сборке
- Объект с финализатором в Gen 0: переживет сборку, попадет в Gen 1, удален не раньше сборки Gen 1
Что такое управляемые и неуправляемые ресурсы (managed и unmanaged)? Управляемые кем?
Управляемые .NET Runtime — находятся в памяти, про которую знает GC
Управляемые (99.999% того, с чем мы работаем):
- Все обычные объекты C#
- Массивы, строки, коллекции
- Объекты-обертки (FileStream, SqlConnection)
Неуправляемые (прямые ресурсы ОС):
- Память вне кучи: Marshal.AllocHGlobal
- Хэндлы Windows: CreateFile, CreateMutex
- COM объекты
- Выглядят как: IntPtr, void*, HANDLE
HttpClient, FileStream, SqlConnection — управляемые или неуправляемые?
Это управляемые объекты, но они оборачивают неуправляемые ресурсы
FileStream fileStream = new FileStream(...);
// fileStream — управляемый объект в куче
// Внутри хранит IntPtr hFile — неуправляемый хэндл файла
Что такое using и во что разворачивается?
using — синтаксический сахар для автоматического вызова Dispose()
using (var file = new FileStream("test.txt", FileMode.Open))
{
// работа с файлом
}
Разворачивается в:
FileStream file = null;
try
{
file = new FileStream("test.txt", FileMode.Open);
// работа с файлом
}
finally
{
if (file != null)
file.Dispose();
}
Важно: using корректно обработает null!
using (IDisposable resource = null) // не вызовет исключение
{
// код выполнится нормально
}
C# 8.0 — using declaration:
using var file = new FileStream("test.txt", FileMode.Open);
// Dispose вызовется в конце области видимости
Зачем нужен IDisposable, если есть финализатор?
IDisposable — это оптимизация для своевременного освобождения ресурсов
IDisposable (детерминированное освобождение):
- Освобождение ресурсов прямо сейчас
- Контролируемое время и место вызова
- Выполняется на текущем потоке
- Это ОПТИМИЗАЦИЯ — мы не ждем неопределенное время
Финализатор (недетерминированное):
- Неизвестно когда выполнится
- Отдельный поток
- Может вообще не выполниться
Пример важности оптимизации: если только 10 подключений к БД доступно одновременно, без IDisposable они будут заняты до срабатывания финализатора (может быть минуты или часы). С IDisposable — освобождаем сразу после использования
IDisposable позволяет нам явно сказать "я закончил работу с ресурсом СЕЙЧАС", а не полагаться на GC
Если не вызвать Dispose вручную, он вызовется автоматически?
Нет! Dispose НИКОГДА не вызывается автоматически (кроме using)
Частая ошибка:
SqlConnection conn = new SqlConnection(connString);
// ... используем
// Забыли Dispose() — соединение висит до финализатора!
Какие гарантии вызова у Dispose() по сравнению с финализатором?
Dispose:
- Вызывается когда МЫ решили
- Гарантированно выполнится (если мы вызвали)
- Можем обработать исключения
Финализатор:
- Вызывается когда GC решит
- Может не выполниться при завершении процесса
- Исключения игнорируются
Когда использовать Dispose, а когда финализатор?
Dispose: ВСЕГДА для любых ресурсов требующих освобождения
Финализатор: ТОЛЬКО когда напрямую работаем с неуправляемыми ресурсами (практически никогда)
Правило: если используешь чужой класс с Dispose() — вызывай Dispose(). Если пишешь свой класс — финализатор нужен только для прямой работы с IntPtr/хэндлами
Что такое утечки памяти? Откуда они берутся, если есть GC? Как их обнаружить?
Ситуация, когда объекты больше не нужны приложению, но остаются достижимыми от GC Roots
Частые причины:
- Забытая подписка на события
- Статические коллекции без очистки
- Замыкания, захватывающие большие объекты
- Кэши без ограничения размера
Пример утечки через события:
public class Publisher
{
public event Action LongLivedEvent;
}
publisher.LongLivedEvent += myObject.HandleEvent;
// myObject не будет собран, пока жив publisher!
Обнаружение:
- Visual Studio Diagnostic Tools
- JetBrains dotMemory
- PerfView (бесплатный от Microsoft)
Что такое card table при сборке мусора?
Card table — оптимизация для отслеживания ссылок из старших поколений в младшие
Проблема: при сборке Gen 0 нужно найти все ссылки из Gen 1/2 на объекты Gen 0, но сканировать все старшие поколения дорого
Решение:
- Память делится на "карты" по 128 байт
- При записи ссылки JIT помечает карту "грязной"
- При сборке проверяются только грязные карты
Как работает:
class Parent // в Gen 2
{
public Child child;
}
parent.child = new Child(); // Gen 0 объект
// JIT автоматически вставляет код пометки карты
При сборке Gen 0 GC смотрит только помеченные участки Gen 2
Что такое Frozen Object Heap (FOH)?
Специальная куча для неизменяемых объектов, живущих весь срок работы приложения (.NET 8+)
Объекты в FOH:
- Никогда не собираются GC
- Не сканируются при сборке мусора
- Оптимизированы для чтения
- Занимают меньше памяти (нет заголовков для GC)
Что попадает автоматически:
- Строковые литералы
- Type объекты
- Assembly metadata
- Некоторые статические readonly данные
Пример:
// Эта строка в FOH
const string Literal = "Hello";
// Эта строка в обычной куче
string dynamic = new string('H', 5);
Как правильно реализовать паттерн Dispose с финализатором?
Паттерн нужен ТОЛЬКО при прямой работе с неуправляемыми ресурсами (IntPtr, хэндлы). В 99.9% случаев НЕ нужен!
public class UnmanagedWrapper : IDisposable
{
private IntPtr unmanagedHandle;
private bool disposed = false;
public UnmanagedWrapper()
{
unmanagedHandle = NativeMethods.CreateHandle();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Освобождаем управляемые ресурсы
// Например, другие IDisposable поля
}
// Освобождаем НЕуправляемые ресурсы
if (unmanagedHandle != IntPtr.Zero)
{
NativeMethods.ReleaseHandle(unmanagedHandle);
unmanagedHandle = IntPtr.Zero;
}
disposed = true;
}
}
// Финализатор ТОЛЬКО если есть неуправляемые ресурсы!
~UnmanagedWrapper()
{
Dispose(false);
}
}
Частая ошибка: добавлять финализатор "на всякий случай" для обычных управляемых ресурсов
+++++