Что такое Pinned Object Heap (POH) в .NET?

«Что такое Pinned Object Heap (POH) в .NET?» — вопрос из категории Управление памятью, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Pinned Object Heap (POH) — это специальный регион управляемой кучи, введённый в .NET 5, предназначенный для хранения объектов, которые должны быть закреплены (pinned) в памяти. Закрепление предотвращает перемещение объекта сборщиком мусора (GC), что необходимо при передаче указателя на управляемую память в неуправляемый код (например, через P/Invoke или при работе с IO).

Проблема, которую решает POH: Раньше для закрепления использовался оператор fixed или GCHandle. Это приводило к:

  1. Фрагментации кучи: Закреплённый объект в обычной куче (SOH/LOH) блокировал компактизацию GC, создавая "дыры".
  2. Накладным расходам: Процесс закрепления/открепления требовал дополнительных операций.

Как работает POH:

  • Объекты, выделенные в POH, никогда не перемещаются GC.
  • Это позволяет передавать их указатели в нативный код напрямую, без дополнительного закрепления.
  • POH не компактируется, что исключает фрагментацию основной кучи из-за pinned-объектов.

Практическое использование:

using System;
using System.Runtime.InteropServices;

// Пример нативного метода, работающего с буфером
[DllImport("NativeLib.dll")]
private static extern int ProcessBuffer(byte[] buffer, int length);

public void WorkWithPinnedBuffer()
{
    // 1. Выделяем буфер в Pinned Object Heap (pinned: true)
    // Размер 4096 байт, массив считается "долгоживущим" и закреплённым.
    byte[] buffer = GC.AllocateArray<byte>(length: 4096, pinned: true);

    // 2. Заполняем буфер данными (например, из файла или сети)
    new Random().NextBytes(buffer);

    // 3. Передаём в неуправляемый код. GC не будет двигать массив,
    // поэтому указатель остаётся валидным на всё время вызова.
    int result = ProcessBuffer(buffer, buffer.Length);

    Console.WriteLine($"Native code processed buffer, result: {result}");

    // 4. Важно: память в POH освобождается только при сборке мусора
    // поколения 2 или при выгрузке домена. Явно "удалить" объект нельзя.
    buffer = null; // Делаем объект доступным для GC
    // Рекомендуется для долгоживущих или крупных pinned-буферов.
}

Ключевые моменты и рекомендации:

  • Когда использовать: Для буферов, которые часто передаются в нативный код (работа с файлами, сетевыми пакетами, графическими API).
  • Выделение: Только через GC.AllocateArray<T>(int length, bool pinned). Обычный new не размещает объекты в POH.
  • Очистка: Память в POH не освобождается сразу, когда объект становится недостижим. Она освобождается при полной сборке мусора (Gen 2). Не используйте POH для короткоживущих объектов.
  • Наблюдение: За POH можно следить через счетчики производительности .NET или события GC.GetGCMemoryInfo().PinnedObjectsMemorySize.