Вы когда-нибудь задумывались, где живут программы и почему нам нужны устройства со все большим объемом оперативной памяти? Ну, у вас, вероятно, есть какие-то «большие» и «тяжелые» программы на вашем компьютере, например, для обработки изображений или видео. А может у вас есть какая-то игра с высокими требованиями к запуску? Что общего между этими двумя типами программного обеспечения? И что происходит, когда вы их запускаете?

Основы памяти

Ну, во-первых, они, вероятно, оба занимают много места на жестком диске. Но что более важно, оба случая требуют высокой вычислительной мощности для запуска и выполнения своей работы. Происходит то, что пока вы не запустите программу, она живет в файловой системе (HDD или SSD) компьютера. Это означает, что он просто хранится там и останется там, пока вы его не удалите, он не требует вычислительной мощности для запуска процессов или задач. Это похоже на то, что ваш учебник остается на вашем столе. Пока вы не возьмете его и не откроете, вы его не изучите и ничего с ним не сделаете. Он просто лежит и занимает место.

Но когда вы начинаете свою программу, начинается все самое интересное. Ваша программа запрашивает у операционной системы количество ресурсов, необходимых для ее первоначальной загрузки и запуска. Ресурс называется оперативной памятью, и это место, где живет ваша программа, когда вы ее используете. Тот объем оперативной памяти, который программа принимает во время использования, может увеличиваться, если программе требуется больше ресурсов, или может уменьшаться, если они ей не нужны. Основная причина, по которой ваш компьютер использует ОЗУ вместо памяти для хранения, заключается в том, что ОЗУ работает быстрее. Намного быстрее. Например, оперативная память DDR4 может передавать до 19 000 МБ/с, в отличие от SSD, где вы можете передавать что-то около 500 МБ/с. Итак, возникает вопрос, почему бы нам не использовать оперативную память для всего, если она намного быстрее? Что ж, проблема с оперативной памятью заключается в том, что она сбрасывается при каждой перезагрузке или выключении вашего компьютера, а это означает, что все, что в ней хранится, теряется. Вот почему компьютерам нужен жесткий диск для более постоянного хранения.

Стек и куча

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

Изначально все программы C# начинаются с одного и того же размера стека для каждого потока — это 1 МБ для 32-разрядных систем и 4 МБ для 64-разрядных систем. Если вас больше интересует, почему так, вы можете посмотреть здесь. Если во время выполнения вашей программы вы не попросите ОС изменить размер стека, его размер останется неизменным для каждого используемого вами потока. С другой стороны, куча более динамична. Его размер зависит от операций, которые вы пытаетесь выполнить. Например, буферизация файлов, подключения к базе данных и построитель строк — это операции, которые потребляют больше памяти и могут привести к OutOfMemmoryException. Это исключение, которое прерывает выполнение вашей программы и происходит, когда ОС отказывается выделять больше памяти для вашего приложения. Но поскольку размер кучи не является фиксированным и ограничен только размером вашей оперативной памяти, это не так распространено, как исключение StackOverflowException, которое возникает всякий раз, когда вы пытаетесь превысить лимит стека (1 МБ или 4 МБ в зависимости от процессора). архитектура). Обычно StackOverflowException возникает, когда у вас очень глубокая или несвязанная рекурсия или слишком много вызовов вложенных методов, как описано здесь.

Куча, как упоминалось выше, представляет собой область, выделенную для оперативной памяти вашего приложения, где живут ваши эталонные типы данных. Он начинается как компактная и непрерывная структура адресов, взятых из начальной загрузки вашего приложения. Есть указатель, указывающий на последний взятый адрес из блока. Каждый раз, когда мы создаем новый объект, берется следующий адрес, и этот указатель перемещается на следующий свободный адрес. На данный момент у нас есть наши объекты, и все они имеют ссылку, указывающую на данные из кучи. Но что происходит, когда у нас не осталось ссылок на наши данные (адрес памяти)? Теперь мы сохранили некоторые данные, которые никто не использует и не нуждается. Эти блоки памяти помечаются для очистки сборщиком мусора. Затем они освобождаются, а все «занятые» адреса перераспределяются, так что мы снова можем иметь непрерывную и компактную структуру. Эти три этапа называются: «отметить», «развернуть» и «распределить». Наконец, после фазы выделения указатель перемещается на последний занятый адрес. Как вы видете

это дорогостоящий процесс, и поэтому он не происходит постоянно. Он срабатывает в трех случаях:

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

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

- Вызывается метод GC.Collect(). Обычно это ручная операция.

Стек, с другой стороны, более структурирован. Память на короткую жизнь всегда важнее памяти на долгую жизнь. Это означает, что память, взятая из объектов на вершине стека, будет очищена раньше, чем память, взятая из объектов в нижней части стека (принцип ЛИФО — последний пришел, первый ушел). Это ясно видно из метода, называемого стеком программы. После завершения метода (область действия определяется фигурными скобками) вся занимаемая им память освобождается. Это делает стек более подходящим для хранения ссылок и типов значений, как упоминалось в начале. Нам не нужно запускать дорогостоящий процесс GC для очистки типов значений, когда мы можем поместить их в стек и сделать это автоматически. Вы можете выделить типы значений в куче, если хотите. Этот процесс называется упаковкой, а противоположный процесс (где вы выделяете типы значений из кучи в стек) называется распаковкой.

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

Теперь, когда мы выяснили, что такое стек и куча, давайте посмотрим на два типа переменных и их поведение. Как упоминалось в C#, у нас есть типы значений и ссылочные типы. Имена указывают, как копируются данные. Переменная типа значения содержит экземпляр типа — int, long, bool, etc. Это отличается от переменной ссылочного типа, которая содержит ссылку на экземпляр типа.

Вот почему, когда мы утверждаем, что B = A, в приведенном выше примере мы копируем значение A и устанавливаем его в B. Но поскольку это значение копируется, в стеке есть две отдельные переменные со значением 5 и две разные имена. После присвоения B мы меняем значение A, и, как и ожидалось, значение B остается неизменным.

С другой стороны, ссылочные типы содержат ссылку на экземпляр типа. Когда мы создаем экземпляр ссылочного типа (класса, строки или делегата), мы выделяем память из кучи для хранения данных, а в переменной cat1 (которая находится в стеке) мы храним только ссылку (адрес) того места, где выделяются реальные данные.

Это похоже на ваш адрес в вашем удостоверении личности, он не содержит вашего дома или вещей, но указывает, где их можно найти. И когда вы записываете свой адрес в другой документ, это как копирование ссылки на реальные данные. Теперь, поскольку мы копируем только ссылку при изменении имени cat1, имя cat2 также изменяется, потому что у нас есть два указателя на один набор данных.

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

Два способа создания копии называются «поверхностной» копией и «глубокой» копией. Поверхностная копия копирует только ссылку. Глубокая копия — это когда вы выделяете новую память и копируете все поля объекта. Помните, что у вас могут быть вложенные объекты, для которых вам также потребуется создавать глубокие копии. В противном случае вы снова измените один и тот же источник данных. Это показано на примере ниже. Там мы делаем неглубокую копию (мы копируем только ссылку) на хвост cat1 и когда мы его меняем, мы также меняем длину хвоста cat2.

Поведение при вызове метода

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

Различные типы имеют разное поведение при передаче вызовов методов. Как мы уже говорили, типы значений копируются значением, поэтому любые изменения внутри вызова метода не влияют на начальное значение, которое было передано в метод. И все, что происходит с этим значением, остается в рамках этого метода. Ссылочные типы, с другой стороны, копируются по ссылке, поэтому, когда мы передаем их методу, мы можем изменить внешний объект. Мы копируем адрес данных и передаем его методу, но источник данных остается в куче прежним. Таким образом, любые изменения объекта, которые мы делаем в методе, влияют на другие ссылки на эти данные. В нашем случае это две ссылки: cat — в основном методе и reference type — в методе PrintVariables.

Заключение

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