Готовый перевод Under the Hood of .NET Memory Management / Под капотом у памяти в .Net: Section 1: Introduction to .NET Memory Management
Глава 1: Введение.
.NET Framework -- это шедевр программной инженерии. Он настолько хорош, что вы можете начать программировать с минимальными знаниями о том, как все устроено изнутри. О значительной части сложных вещей (таких, например, как управление памятью) уже позаботились, так что вы можете сфокусироваться на написании кода и задачах, которые вы хотите решить. "Тебе не нужно волноваться о выделении памяти в .NET" -- распространенное заблуждение. В итоге, языки программирования платформы .NET, такие, как C# и VB.NET, легче выучить, и многие разработчики успешно используют существующие навыки со стеком технологий .NET.
Недостатком этого подхода является то, что вы можете невероятно хорошо знать синтаксис языка и полезные классы фреймворка, но практически не иметь представления о влиянии того, что вы пишете, на производительность и память. Эти "пробелы в знаниях" обычно всплывают под конец процесса разработки проекта, при нагрузочном тестировании, или даже когда продукт уже используется пользователями и возникла какая-то проблема.
Существуют тысячи приложений, написанных разработчиками такой квалификации. Если вы читаете эту книгу, то, вероятно, вы - один из них; поэтому, чтобы закрыть пробелы в ваших знаниях, мы отправимся в путешествие к глубинам управления памятью в среде исполнения .NET.
Вы узнаете, как приложения на самом деле работают под капотом. Когда вы поймете основы управления памятью, вы станете писать более быстрый, более эффективный -- лучше, чем раньше -- код, и будете намного лучше подготовлены к ситуации, когда в одном из ваших приложений возникнет какая-то проблема.
Итак, для того чтобы все прошло гладко, мы будем потихоньку развивать свои знания с нуля. Вместо того, чтобы сразу переходить к сложным определениям и деталям реализации, я собираюсь начать с упрощенной модели и добавлять сложность по мере прочитанного материала, от главы к главе. При таком подходе к концу книги вы будете иметь уверенное понимание всех тем которые будут рассказаны.
На самом деле, к концу книги мы рассмотрим управление памятью вплоть до уровня операционной системы, но мы будем делать это по одному шагу за раз. Как говорится, «путешествие в тысячу миль начинается с одного шага».
Беглый обзор
Если задуматься, любое приложение состоит из двух вещей: самого кода; и данных, хранящих состояние приложения во время выполнения. Когда приложение для .NET запускается, для хранения данных создаются четыре блока памяти (кучи):
•куча кода хранит нативные инструкции процессора после того, как код был JIT-скомпилирован
•куча малых объектов(SOH) хранит объекты, меньшие 85 Кб
•куча больших объектов(LOH) хранит объекты, большие 85 Кб (хотя тут есть некоторые исключения, см. Главу 2)
•наконец, еще есть куча процесса, но это отдельная история
Все, что находится в куче, имеет адрес; эти адреса используются для отслеживания выполнения программы и изменений состояния приложения.
Обычно приложения инкапсулируют код в методы и классы, поэтому .NET необходимо отслеживать цепочки вызовов методов, и состояние данных, хранимое в каждом из этих вызовов методов.
Когда метод вызывается, он находится в своем окружении, как в коконе, а все переменные, которые он создает, существуют только до конца вызова этого метода.
Метод также может получать данные из глобальных/статических объектов, и параметров, которые были ему переданы.
К тому же, когда один метод вызывает другой, состояние вызывающего метода(переменные, параметры) сохраняется, пока вызванный метод выполняется. Когда выполнение вызванного метода завершается, оригинальное состояние вызвавшего должно быть восстановлено, чтобы он мог продолжить выполнение.
Чтобы за всем следить (и этого "всего" часто довольно много), .NET поддерживает специальную структуру данных -- стек, который используется для отслеживания состояния выполнения потока и всех сделанных вызовов методов.
Стек
Итак, стек используется для отслеживания данных методов и адресов возврата из них. Когда вызывается метод, .NET создает контейнер (фрейм стека), который содержит все данные, необходимые для выполнения вызова, включая параметры метода, локальные переменные и адрес строки кода, которая будет выполнена после завершения метода. Для каждого вызова метода выполненного внутри дерева вызовов (когда один метод вызывает другой, который вызывает другой и т.д.), стековые фреймы накладываются друг на друга. Когда метод завершается, его фрейм удаляется из верхней части стека, и выполнение возвращается к следующей строке кода в вызывающем методе (со своим собственным фреймом стека). Верхний фрейм стека используется методом исполняемым в текущий момент.
Используя этот простой механизм, состояние каждого метода сохраняется между вызовами других методов, и все они хранятся отдельно друг от друга.
В листинге 1.1 Method1 вызывает Method2, передавая в качестве параметра значение типа int. Чтобы вызвать Method2, поток приложения должен сначала записать адрес возврата, который является следующей строкой кода после вызова Method2. Когда Method2 завершает выполнение, этот адрес используется для вызова следующей строки кода в Method1(строка 4). Адрес возврата, следовательно, помещается в стек.
Параметры, передаваемые в Method2, также помещаются в стек. Наконец, мы готовы перейти к выполнению по адресу кода метода Method2.
Если мы поместим точку останова на строке 13, стек будет выглядеть примерно так, как показано на рисунке 1.1. Очевидно, это огромное упрощение, и в качестве адресов возврата не будут использоваться номера строк кода, но, надеюсь, вы поняли идею.
На рисунке 1.1 стековые кадры для методов 1, 2 и 3 размещены друг над другом, и в настоящий момент текущий стековый кадр принадлежит методу Method3, который является текущим исполняемым методом. Когда Method3 завершает выполнение, его кадр стека удаляется, точка выполнения программы перемещается обратно в Method2 (строка 9 в листинге 1), и выполнение продолжается.
Хорошее и простое решение сложной задачи, но не забывайте - что, если ваше приложение имеет несколько потоков, то каждый поток будет иметь свой собственный стек.
Итак, где же находится куча данных? В стеке могут храниться переменные, которые представляют собой примитивные типы данных, определенные в .NET. К ним относятся следующие типы (см.http://tinyurl.com/FrameworkOverview): Примитивные типы данных, часть общей системы типов (CTS), изначально понятные всем компиляторам языка .NET, а так же все экземпляры типов значений. Любой экземпляр этих типов данных обычно хранится в стеке.
С другой стороны, экземпляры ссылочных типов, в том числе:
• классы
• интерфейсы
• делегаты
• строки
• экземпляры «объектов»
... все они хранятся в куче (SOH или LOH, в зависимости от их размера).
Когда создается экземпляр ссылочного типа, в стеке сохраняется только ссылка на объект. Сам фактический экземпляр создается в куче, а его адрес хранится в стеке.
Рассмотрим следующий код:
В листинге 1.2 новый экземпляр класса MyClass создается внутри метода Method1. Как мы видим на рисунке 1.2, для этого .NET должен создать объект в куче, определить его адрес в куче и поместить эту ссылку на объект в кадр стека метода Method1. Пока выполняется Method1, объект, выделенный в куче, будет иметь ссылку, хранящуюся в стеке. Когда Method1 завершает свое выполнение, стековый кадр удаляется (вместе со ссылкой на объект), оставляя объект без ссылки(корня).
Позже мы увидим, как это влияет на управление памятью и сборщик мусора.
Более подробно о типах значений и ссылочных типах
Рассмотрим код в листинге 1.3.
Если поместить точка останова на строке 6, то стек / куча будут выглядеть так, как показано на рисунке 1.3.
В стеке расположены два целых числа с одинаковым значением. Обратите внимание, что есть две переменные стека, v1 и v2, и все, что было сделано, - присвоение им одинакового значения.
Давайте посмотрим на похожий сценарий, на этот раз использующий класс, который я определил как MyClass, который (очевидно) является ссылочным типом: размещение точки останова в строке 5 в листинге 1.4 приведет к тому, что два экземпляра класса MyClass будут размещены в куче.
Давайте теперь поговорим на ту тему, о которой вас всегда спрашивают на собеседовании - упаковке и распаковке. На самом деле это действительно легко понять, и просто заключается в дополнительной работе, которая требуется, когда ваш код размещает экземпляры типов значений (например, int, char и т. д.) в куче, а не на стеке. Как мы видели ранее, размещение в куче более затратно и, следовательно, менее эффективно.
Классический пример упаковки и распаковки выглядит примерно так:
В листинге 1.9 целое число при объявлении размещается в стеке, потому что это тип значения (строка 2). Затем он назначается новой переменной типа object, которая является ссылочным типом (строка 4), и поэтому для целого числа выделяется новый объект в куче. Наконец, целое число распаковывается из кучи и присваивается целочисленной переменной стека (строка 6).
Вопрос, который смущает всех - зачем вам это делать ведь это не имеет никакого смысла. Ответ на этот вопрос заключается в том, что вы можете произвести упаковку типов значений, даже не подозревая об этом.
В листинге 1.10 показано, как может происходить незаметная упаковка и распаковка, и я уверен, что в какой-то момент вы писали похожий код. Добавление целого числа (тип значения) к ArrayList приведет к выполнению операции упаковки, так как, чтобы создать динамический массив для любых типов, метод Add принимает object в качестве параметра. Итак, чтобы добавить целое число в ArrayList, оно должен быть размещено в куче.
При обращении к целому числу в строке 6 создается новая переменная стека "p", и ее значение устанавливается равным первому целому числу в ArrayList.
Короче говоря, выполняется намного больше работы, чем необходимо, и если бы вы делали это в цикле с тысячами целых чисел, производительность была бы значительно ниже.
Еще кое-что про кучу
Сборка мусора
Все вместе они называются корневыми ссылками.
Thread Statics
Иногда вам может понадобиться запретить нескольким потокам доступ к разделяемым статическим полям. Для этого вы можете добавить атрибут [ThreadStatic] к члену и создать несколько статических экземпляров этого члена - по одному для каждого изолированного потока (по одному экземпляру на поток), как показано в листинге 1.16.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: идёт перевод
Переведено на Нотабеноиде
http://notabenoid.org/book/78130/456341
Переводчики: masterofsea147, quandisti
Настройки
Готово:
