Under the Hood of .NET Memory Management / Под капотом у памяти в .Net: Section 1: Introduction to .NET Memory Management

← →
Английский оригинал Перевод на русский

Сожалеем, но текст оригинала доступен только зарегистрированным пользователям.

Глава 1: Введение.
.NET Framework -- это шедевр программной инженерии. Он настолько хорош, что вы можете начать программировать с минимальными знаниями о том, как все устроено изнутри. О значительной части сложных вещей (таких, например, как управление памятью) уже позаботились, так что вы можете сфокусироваться на написании кода и задачах, которые вы хотите решить. "Тебе не нужно волноваться о выделении памяти в .NET" -- распространенное заблуждение. В итоге, языки программирования платформы .NET, такие, как C# и VB.NET, легче выучить, и многие разработчики успешно используют существующие навыки со стеком технологий .NET.

quandisti 20.11.19 в 02:30

0
+1

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

quandisti 20.11.19 в 02:33

0

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

quandisti 20.11.19 в 02:36

0

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

quandisti 20.11.19 в 02:39

0

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

masterofsea147 16.12.19 в 07:34

0

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

masterofsea147 16.12.19 в 07:37

0

Беглый обзор
Если задуматься, любое приложение состоит из двух вещей: самого кода; и данных, хранящих состояние приложения во время выполнения. Когда приложение для .NET запускается, для хранения данных создаются четыре блока памяти (кучи):
•куча кода хранит нативные инструкции процессора после того, как код был JIT-скомпилирован
•куча малых объектов(SOH) хранит объекты, меньшие 85 Кб
•куча больших объектов(LOH) хранит объекты, большие 85 Кб (хотя тут есть некоторые исключения, см. Главу 2)
•наконец, еще есть куча процесса, но это отдельная история

quandisti 20.11.19 в 10:17

0
+1

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

quandisti 20.11.19 в 10:18

0

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

quandisti 20.11.19 в 10:24

0

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

quandisti 20.11.19 в 10:26

0

Чтобы за всем следить (и этого "всего" часто довольно много), .NET поддерживает специальную структуру данных -- стек, который используется для отслеживания состояния выполнения потока и всех сделанных вызовов методов.

quandisti 20.11.19 в 10:28

0

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

quandisti 21.11.19 в 03:16

0

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

masterofsea147 16.12.19 в 07:49

0

В листинге 1.1 Method1 вызывает Method2, передавая в качестве параметра значение типа int. Чтобы вызвать Method2, поток приложения должен сначала записать адрес возврата, который является следующей строкой кода после вызова Method2. Когда Method2 завершает выполнение, этот адрес используется для вызова следующей строки кода в Method1(строка 4). Адрес возврата, следовательно, помещается в стек.

masterofsea147 16.12.19 в 10:27

0

Параметры, передаваемые в Method2, также помещаются в стек. Наконец, мы готовы перейти к выполнению по адресу кода метода Method2.

masterofsea147 16.12.19 в 10:48

0

Если мы поместим точку останова на строке 13, стек будет выглядеть примерно так, как показано на рисунке 1.1. Очевидно, это огромное упрощение, и в качестве адресов возврата не будут использоваться номера строк кода, но, надеюсь, вы поняли идею.

masterofsea147 20.12.19 в 10:35

0

На рисунке 1.1 стековые кадры для методов 1, 2 и 3 размещены друг над другом, и в настоящий момент текущий стековый кадр принадлежит методу Method3, который является текущим исполняемым методом. Когда Method3 завершает выполнение, его кадр стека удаляется, точка выполнения программы перемещается обратно в Method2 (строка 9 в листинге 1), и выполнение продолжается.

masterofsea147 20.12.19 в 10:40

0

Хорошее и простое решение сложной задачи, но не забывайте - что, если ваше приложение имеет несколько потоков, то каждый поток будет иметь свой собственный стек.

masterofsea147 16.12.19 в 10:50

0

Итак, где же находится куча данных? В стеке могут храниться переменные, которые представляют собой примитивные типы данных, определенные в .NET. К ним относятся следующие типы (см.http://tinyurl.com/FrameworkOverview): Примитивные типы данных, часть общей системы типов (CTS), изначально понятные всем компиляторам языка .NET, а так же все экземпляры типов значений. Любой экземпляр этих типов данных обычно хранится в стеке.

masterofsea147 20.12.19 в 10:46

0

С другой стороны, экземпляры ссылочных типов, в том числе:
• классы
• интерфейсы
• делегаты
• строки
• экземпляры «объектов»
... все они хранятся в куче (SOH или LOH, в зависимости от их размера).

masterofsea147 20.12.19 в 10:48

0

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

masterofsea147 20.12.19 в 10:49

0

Рассмотрим следующий код:
В листинге 1.2 новый экземпляр класса MyClass создается внутри метода Method1. Как мы видим на рисунке 1.2, для этого .NET должен создать объект в куче, определить его адрес в куче и поместить эту ссылку на объект в кадр стека метода Method1. Пока выполняется Method1, объект, выделенный в куче, будет иметь ссылку, хранящуюся в стеке. Когда Method1 завершает свое выполнение, стековый кадр удаляется (вместе со ссылкой на объект), оставляя объект без ссылки(корня).

masterofsea147 20.12.19 в 10:52

0

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

masterofsea147 20.12.19 в 10:53

0

Более подробно о типах значений и ссылочных типах

masterofsea147 20.12.19 в 10:56

0
+1

Рассмотрим код в листинге 1.3.
Если поместить точка останова на строке 6, то стек / куча будут выглядеть так, как показано на рисунке 1.3.

masterofsea147 20.12.19 в 10:58

0

В стеке расположены два целых числа с одинаковым значением. Обратите внимание, что есть две переменные стека, v1 и v2, и все, что было сделано, - присвоение им одинакового значения.

masterofsea147 20.12.19 в 11:00

0

Давайте посмотрим на похожий сценарий, на этот раз использующий класс, который я определил как MyClass, который (очевидно) является ссылочным типом: размещение точки останова в строке 5 в листинге 1.4 приведет к тому, что два экземпляра класса MyClass будут размещены в куче.

masterofsea147 20.12.19 в 11:04

0

Давайте теперь поговорим на ту тему, о которой вас всегда спрашивают на собеседовании - упаковке и распаковке. На самом деле это действительно легко понять, и просто заключается в дополнительной работе, которая требуется, когда ваш код размещает экземпляры типов значений (например, int, char и т. д.) в куче, а не на стеке. Как мы видели ранее, размещение в куче более затратно и, следовательно, менее эффективно.

masterofsea147 20.12.19 в 11:09

0

Классический пример упаковки и распаковки выглядит примерно так:
В листинге 1.9 целое число при объявлении размещается в стеке, потому что это тип значения (строка 2). Затем он назначается новой переменной типа object, которая является ссылочным типом (строка 4), и поэтому для целого числа выделяется новый объект в куче. Наконец, целое число распаковывается из кучи и присваивается целочисленной переменной стека (строка 6).

masterofsea147 20.12.19 в 11:40

0

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

masterofsea147 20.12.19 в 11:44

0

В листинге 1.10 показано, как может происходить незаметная упаковка и распаковка, и я уверен, что в какой-то момент вы писали похожий код. Добавление целого числа (тип значения) к ArrayList приведет к выполнению операции упаковки, так как, чтобы создать динамический массив для любых типов, метод Add принимает object в качестве параметра. Итак, чтобы добавить целое число в ArrayList, оно должен быть размещено в куче.

masterofsea147 20.12.19 в 11:48

0

При обращении к целому числу в строке 6 создается новая переменная стека "p", и ее значение устанавливается равным первому целому числу в ArrayList.

masterofsea147 20.12.19 в 11:49

0

Короче говоря, выполняется намного больше работы, чем необходимо, и если бы вы делали это в цикле с тысячами целых чисел, производительность была бы значительно ниже.

masterofsea147 20.12.19 в 11:50

0

Еще кое-что про кучу

quandisti 21.11.19 в 03:16

0

Сборка мусора

quandisti 21.11.19 в 03:16

0

Все вместе они называются корневыми ссылками.

masterofsea147 20.12.19 в 11:51

0
Минутку...

Следующая страница →