Поиск по сайту:

Как работает управление памятью в C#?


По сравнению с C++ сборщик мусора в C# кажется волшебством, и вы можете очень легко писать код, не беспокоясь о базовой памяти. Но если вы заботитесь о производительности, знание того, как среда выполнения .NET управляет своей оперативной памятью, может помочь вам писать более качественный код.

Типы значений и ссылочные типы

В .NET есть два типа типов, которые напрямую влияют на то, как обрабатывается базовая память.

Типы значений — это примитивные типы с фиксированными размерами, такие как int, bool, float, double и т. д. Они передаются по значению, то есть если вы вызываете someFunction(int arg), аргумент копируется и отправляется как новое место в памяти.

Под капотом типы значений (обычно) хранятся в стеке. В основном это относится к локальным переменным, и есть множество исключений, когда они вместо этого хранятся в куче. Но во всех случаях место в памяти, где находится тип значения, содержит фактическое значение этой переменной.

Стек — это просто специальное место в памяти, инициализированное значением по умолчанию, но способное расширяться. Стек представляет собой структуру данных «последний пришел — первый ушел» (LIFO). Вы можете думать об этом как о ведре — переменные добавляются в верхнюю часть ведра, а когда они выходят за пределы области видимости, .NET лезет в ведро и удаляет их по одной, пока не доберется до дна.

Стек намного быстрее, но это все еще просто место в ОЗУ, а не специальное место в кеше ЦП (хотя он меньше, чем куча, и поэтому, скорее всего, будет горячим в кеше, что помогает с производительностью ).

Стек получает большую часть своей производительности от своей структуры LIFO. Когда вы вызываете функцию, все переменные, определенные в этой функции, добавляются в стек. Когда эта функция возвращает значение и эти переменные выходят за пределы области видимости, стек очищает все, что поместила в него функция. Среда выполнения управляет этим с помощью кадров стека, которые определяют блоки памяти для различных функций. Выделение стека происходит очень быстро, потому что просто записывается одно значение в конец фрейма стека.

Отсюда же и термин StackOverflow, который возникает, когда функция содержит слишком много вызовов вложенных методов и заполняет весь стек.

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

Ссылочные типы, такие как экземпляры классов, часто инициализируются ключевым словом new , которое создает новый экземпляр класса и возвращает ссылку на него. Вы можете установить это в локальную переменную, которая фактически использует стек для хранения ссылки на местоположение в куче.

Куча может расширяться и заполняться до тех пор, пока у компьютера не закончится память, что делает ее отличной для хранения большого количества данных. Однако он неорганизован, и в C# для его правильной работы необходимо управлять сборщиком мусора. Выделение кучи также происходит медленнее, чем выделение стека, хотя и достаточно быстро.

Однако из этих правил есть ряд исключений, иначе типы значений и ссылок назывались бы «типами стека» и «типами кучи».

  • Внешние переменные лямбда-функций, локальные переменные блоков IEnumerator и локальные переменные методов async хранятся в куче.
  • Поля типов значений классов являются долговременными переменными и всегда хранятся в куче. Они также заключены в ссылочный тип и хранятся вместе с этим ссылочным типом.
  • Статические поля класса также всегда хранятся в куче.
  • Пользовательские структуры являются типами значений, но они могут содержать ссылочные типы, такие как списки и строки, которые обычно хранятся в куче. Создание копии структуры создает новую копию и выделение всех ссылочных типов в куче.

Наиболее заметным исключением из правила «ссылочные типы в куче» является использование stackalloc с Span, которое вручную выделяет блок памяти в стек для временного массива, который будет очищен от стека как обычно, когда он выйдет за пределы области видимости. Это обходит относительно дорогое выделение кучи и оказывает меньшее давление на сборщик мусора в процессе. Это может быть намного более производительным, но это немного расширенная функция, поэтому, если вы хотите узнать о ней больше, вы можете прочитать это руководство о том, как правильно ее использовать, не вызывая исключения StackOverflow.

Что такое сбор мусора?

Стек очень организован, но куча беспорядочна. Без чего-то, что могло бы управлять этим, вещи в куче не очищаются автоматически, что приводит к тому, что вашему приложению не хватает памяти из-за того, что она никогда не освобождается.

Конечно, это проблема, поэтому существует сборщик мусора. Он работает в фоновом потоке и периодически сканирует ваше приложение на наличие ссылок, которые больше не существуют в стеке, что указывает на то, что программа перестала заботиться о данных, на которые ссылаются. Среда выполнения .NET может выполнять очистку и перемещать память в процессе, чтобы сделать кучу более организованной.

Однако за это волшебство приходится платить — сборка мусора выполняется медленно и дорого. Он работает в фоновом потоке, но есть период, когда выполнение программы должно быть остановлено, чтобы запустить сборку мусора. Это компромисс, который приходит с программированием на C#; все, что вы можете сделать, это попытаться свести к минимуму создаваемый вами мусор.

В языках без сборщика мусора нужно вручную убирать за собой, что во многих случаях быстрее, но больше раздражает программиста. Так что, в некотором смысле, сборщик мусора похож на Roomba, который убирает ваши полы автоматически, но медленнее, чем просто встать и пропылесосить.