Память: стек, куча, виртуальная память

↑ 1.2 ОС и Linux для разработчика · ← Предыдущая · → Следующая

Желательно~4 мин чтенияУровень: junior

Зачем это на собесе

Без понимания стека, кучи и виртуальной памяти не объяснить value и reference типы, boxing, GC, Span<T> и OOM-kill контейнера. Это фундамент для всего раздела .NET runtime.

Подтемы

  • Виртуальная память, RSS
  • Стек
  • Куча
  • Где что лежит в .NET
  • Память в контейнере

Объяснение

Виртуальная память

Каждый процесс видит своё линейное адресное пространство. ОС и MMU отображают виртуальные страницы (обычно 4 КБ) на физическую RAM или своп. Отсюда:

  • изоляция процессов;
  • память выделяется «лениво»: зарезервировать ≠ использовать физически;
  • RSS (resident set size) — реально занятая RAM; именно её ограничивает cgroup в контейнере.

Стек

  • Отдельный на каждый поток, фиксированного размера (в .NET ~1 МБ для основного и пул-потоков, настраивается).
  • Хранит кадры вызовов: локальные переменные, параметры, адрес возврата.
  • Выделение и освобождение мгновенное (сдвиг указателя), очищается автоматически при выходе из метода.
  • Переполнение (глубокая или бесконечная рекурсия) → StackOverflowException, который в .NET нельзя перехватить: процесс падает.

Куча (heap)

  • Общая для всех потоков процесса.
  • Объекты живут, пока на них есть ссылки. В .NET освобождением занимается GC (managed heap).
  • Выделение в .NET быстрое (bump pointer в текущем сегменте), но сборка мусора стоит времени и может ставить потоки на паузу.

Где что лежит в .NET (упрощённо)

  • Reference types (class, массивы, строки, делегаты) — объект всегда в куче, в переменной (на стеке или в поле) — ссылка.
  • Value types (int, struct, DateTime, Guid) — там, где объявлены: локальная переменная — на стеке (или в регистре), поле класса — внутри объекта в куче, элемент массива — внутри массива в куче.
  • Утверждение «value types всегда на стеке» — ошибка, которую любят проверять на собесах.
struct Point { public int X, Y; }          // value type
class Patient { public Point Location; }   // Location хранится ВНУТРИ объекта Patient в куче
 
void Demo()
{
    int count = 5;                  // стек
    Point p = new() { X = 1 };      // стек (сама структура)
    var patient = new Patient();    // ссылка на стеке, объект в куче
    object boxed = count;           // boxing: копия int упакована в объект в куче
}

Память в контейнере

  • Лимит памяти задаёт cgroup. При превышении ядро убивает процесс (OOMKilled, exit code 137).
  • .NET учитывает лимит контейнера: GCHeapHardLimit по умолчанию ~75% лимита.
  • Утечки и большие буферы (загрузка файла целиком в byte[]) — частая причина OOMKilled.

Нюансы и подводные камни

  • Большие файлы (PDF, выгрузки) обрабатывайте потоками (Stream), а не целиком в память: иначе Large Object Heap и риск OOM.
  • stackalloc и Span<T> позволяют работать с буферами на стеке без аллокаций в куче, но размер стека ограничен.
  • Замер памяти: GC.GetTotalMemory показывает только управляемую кучу. RSS процесса больше (нативные буферы, код, стеки).
  • Swap в контейнерах обычно отключён: превышение лимита сразу ведёт к OOMKilled.

Вопросы с ответами

Связанные темы