Может ли экземпляр структуры храниться в куче heap как это сделать
Перейти к содержимому

Может ли экземпляр структуры храниться в куче heap как это сделать

  • автор:

Может ли экземпляр структуры храниться в куче heap как это сделать

Ранее мы рассматривали следующие элементарные типы данных: int, byte, double, string, object и др. Также есть сложные типы: структуры, перечисления, классы. Все эти типы данных можно разделить на типы значений, еще называемые значимыми типами, (value types) и ссылочные типы (reference types). Важно понимать между ними различия.

  • Целочисленные типы ( byte, sbyte, short, ushort, int, uint, long, ulong )
  • Типы с плавающей запятой ( float, double )
  • Тип decimal
  • Тип bool
  • Тип char
  • Перечисления enum
  • Структуры ( struct )
  • Тип object
  • Тип string
  • Классы ( class )
  • Интерфейсы ( interface )
  • Делегаты ( delegate )

В чем же между ними различия? Для этого надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Параметры и переменные метода, которые представляют типы значений, размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек — это некоторая область памяти в адресном пространстве.

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

class Program < static void Main(string[] args) < Calculate(5); >static void Calculate(int t) < int x = 6; int y = 7; int z = y + t; >>

При запуске такой программы в стеке будут определяться два фрейма — для метода Main (так как он вызывается при запуске программы) и для метода Calculate:

Структура стека в языке программирования C#

При вызове этого метода Calculate в его фрейм в стеке будут помещаться значения t, x, y и z. Они определяются в контексте данного метода. Когда метод отработает, область памяти, которая выделялась под стек, впоследствии может быть использована другими методами.

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

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

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

Так, в частности, если мы изменим метод Calculate следующим образом:

static void Calculate(int t)

То теперь значение переменной x будет храниться в куче, так как она представляет ссылочный тип object, а в стеке будет храниться ссылка на объект в куче.

Ссылочные типы в куче в языке программирования C#

Составные типы

Теперь рассмотим ситуацию, когда тип значений и ссылочный тип представляют составные типы — структуру и класс:

State state1 = new State(); // State — структура, ее данные размещены в стеке Country country1 = new Country(); // Country — класс, в стек помещается ссылка на адрес в хипе // а в хипе располагаются все данные объекта country1 struct State < public int x; public int y; >class Country

Здесь в методе Main в стеке выделяется память для объекта state1. Далее в стеке создается ссылка для объекта country1 ( Country country1 ), а с помощью вызова конструктора с ключевым словом new выделяется место в хипе ( new Country() ). Ссылка в стеке для объекта country1 будет представлять адрес на место в хипе, по которому размещен данный объект..

Ссылычные типы и типы значений в C#

Таким образом, в стеке окажутся все поля структуры state1 и ссылка на объект country1 в хипе.

Но, допустим, в структуре State также определена переменная ссылочного типа Country. Где она будет хранить свое значение, если она определена в типе значений?

State state1 = new State(); Country country1 = new Country(); struct State < public int x; public int y; public Country country; public State() < x = 0; y = 0; country = new Country(); >> class Country

Значение переменной state1.country также будет храниться в куче, так как эта переменная представляет ссылочный тип:

Стек и куча в языке программирования C#

Копирование значений

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

State state1 = new State(); // Структура State State state2 = new State(); state2.x = 1; state2.y = 2; state1 = state2; state2.x = 5; // state1.x=1 по-прежнему Console.WriteLine(state1.x); // 1 Console.WriteLine(state2.x); // 5 Country country1 = new Country(); // Класс Country Country country2 = new Country(); country2.x = 1; country2.y = 4; country1 = country2; country2.x = 7; // теперь и country1.x = 7, так как обе ссылки и country1 и country2 // указывают на один объект в хипе Console.WriteLine(country1.x); // 7 Console.WriteLine(country2.x); // 7

Так как state1 — структура, то при присвоении state1 = state2 она получает копию структуры state2. А объект класса country1 при присвоении country1 = country2; получает ссылку на тот же объект, на который указывает country2. Поэтому с изменением country2, так же будет меняться и country1.

Ссылочные типы внутри типов значений

Теперь рассмотрим более изощренный пример, когда внутри структуры у нас может быть переменная ссылочного типа, например, какого-нибудь класса:

State state1 = new State(); State state2 = new State(); state2.country.x = 5; state1 = state2; state2.country.x = 8; // теперь и state1.country.x=8, так как state1.country и state2.country // указывают на один объект в хипе Console.WriteLine(state1.country.x); // 8 Console.WriteLine(state2.country.x); // 8 struct State < public int x; public int y; public Country country; public State() < x = 0; y = 0; country = new Country(); // выделение памяти для объекта Country >> class Country

Переменные ссылочных типов в структурах также сохраняют в стеке ссылку на объект в хипе. И при присвоении двух структур state1 = state2; структура state1 также получит ссылку на объект country в хипе. Поэтому изменение state2.country повлечет за собой также изменение state1.country.

Объекты классов как параметры методов

Организацию объектов в памяти следует учитывать при передаче параметров по значению и по ссылке. Если параметры методов представляют объекты классов, то использование параметров имеет некоторые особенности. Например, создадим метод, который в качестве параметра принимает объект Person:

Person p = new Person < name = "Tom", age = 23 >; ChangePerson(p); Console.WriteLine(p.name); // Alice Console.WriteLine(p.age); // 23 void ChangePerson(Person person) < // сработает person.name = "Alice"; // сработает только в рамках данного метода person = new Person < name = "Bill", age = 45 >; Console.WriteLine(person.name); // Bill > class Person

При передаче объекта класса по значению в метод передается копия ссылки на объект. Эта копия указывает на тот же объект, что и исходная ссылка, потому мы можем изменить отдельные поля и свойства объекта, но не можем изменить сам объект. Поэтому в примере выше сработает только строка person.name = «Alice» .

А другая строка person = new Person < name = "Bill", age = 45 >создаст новый объект в памяти, и person теперь будет указывать на новый объект в памяти. Даже если после этого мы его изменим, то это никак не повлияет на ссылку p в методе Main, поскольку ссылка p все еще указывает на старый объект в памяти.

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

Person p = new Person < name = "Tom", age = 23 >; ChangePerson(ref p); Console.WriteLine(p.name); // Bill Console.WriteLine(p.age); // 45 void ChangePerson(ref Person person) < // сработает person.name = "Alice"; // сработает person = new Person < name = "Bill", age = 45 >; > class Person

Операция new создаст новый объект в памяти, и теперь ссылка person (она же ссылка p из метода Main) будет указывать уже на новый объект в памяти.

Популярные заблуждения о C#

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

Та статья представляла из себя практически «идеальную подборку заблуждений в вакууме». Причём они (заблуждения) являются довольно популярными и постоянно встречаются в различных блогах и подборках «99 вопросов для собеседования», «как пройти собеседование на джуниора» или в данном случае «шпаргалка по C#».

Почему они такие популярные? Я считаю, что потому, что они дают простые и короткие ответы, которые очень удобны для формата теста или квиза. Думать не надо, можно просто запомнить. Практикующие же разработчики повторяют их потому что эти заблуждения часто описывают наблюдаемое положение дел, но это не значит, что они истинны. Если посмотреть в окно в центре города, то можно сделать вывод что все машины — легковые, и это в большинстве случаев будет верно, но к правде имеет довольно слабое отношение.

Мне хочется, чтобы данная статья была не чисто моим мнением и не очередной компиляцией блогов. Поэтому я проведу аналогию (весьма сомнительную, но мне так захотелось) с юриспруденцией. У нас будет так:

Законопроекты — информация о версиях языка новее, чем в ECMA 334, а также ещё не выпущенных версиях. Эту информацию можно найти на гитхабе dotnet, а так же в статьях-анонсах на MSDN.

Подзаконные акты — документация (не статьи) MSDN.

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

Прямой опыт — то, что мы можем просто взять и проверить. Тут нам повезло больше, чем юристам, ведь нам не придётся что-то красть, чтобы проверить на сколько лет за это посадят ))

Не долго думая, возьмём план статьи отсюда и будем рассматривать мифы по списку. Если что-то забуду, добавляйте в комментах.

Ссылочные и значимые типы (value vs reference types)

Заблуждение: ссылочные типы (reference type) хранятся в куче (heap), а значимые (value types) — на стэке.

Почему распространено? Это — простое объяснение, и оно часто тиражируется. Более того, если написать простой метод с простыми переменными одного и другого типа, чем обычно его и иллюстрируют, то всё именно так и будет.

Закон: во всём стандарте есть только 2 упоминания слова «куча», и это вполне объяснимо, поскольку стэк и куча являются деталями реализации, а не самого языка. Второе упоминание — про то, что зафиксированные (fixed) объекты могут приводить к фрагментации кучи, это нам пока не интересно. А первое упоминание — в разделе 16.1 Structs/General, то есть общем описании, а не определении:

However, unlike classes, structs are value types and do not require heap allocation

Это означает только то, что структуры не требуют выделения на куче (но не означает что они располагаются на стэке).

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

Для классов (ссылочных типов) на текущий момент действительно верно, что во всех простых случаях они окажутся размещёнными в куче. Но ссылочный тип чисто теоретически можно разместить на стэке. Более того, скоро это изменится вполне официально, так же спасибо @VladD-exrabbit за вот эту ссылку: https://github.com/dotnet/runtime/issues/11192

P.S.: ещё спасибо @PsyHaSTe за ссылки: один, два, три. «Как можно видеть, Липперта эти сравнения бесили ещё в 2009. »

Так чем же отличаются value и reference типы?

Читаем стандарт (в нём всё про значимые типы лежит в разделе Structs и слово struct используется для их описания):

16.4.2 Value semantics

A variable of a struct type directly contains the data of the struct, whereas a variable of a class type contains a reference to an object that contains the data…

Тут текст про то, что структуры содержат в себе сами данные, а переменная со ссылочным типом — только ссылку. А значит структура не может в себе содержать поля, размер которых ещё не известен (в том числе своего же типа):

struct Node < int data; Node next; // error, Node directly depends on itself >// is an error because Node contains an instance field of its own type. Another example struct A < B b; >struct B < C c; >struct C

With classes, it is possible for two variables to reference the same object…

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

Это всё, что описано в разделе про семантику, а для языка это самое главное. Дальше идёт описание конкретно структур (16.4.3 Inheritance) а также свойства, вытекающие из семантики (16.4.4 Assignment — копирование данных при присваивании), 16.4.5 Default values — значение по умолчанию, 16.4.6 Boxing and unboxing — если нам надо передать ссылку, то требуется боксинг. А так же конструкции языка 16.4.7 Meaning of this, 16.4.8 Field initializers, 16.4.9 Constructors, 16.4.10 Static constructors, 16.4.11 Automatically implemented properties.

На собеседовании на вопрос о различии этих типов главное ответить, что у них разная семантика. Можно, конечно, погрузиться в дальнейшие различия (по списку из стандарта), но нигде среди них нет упоминания, что одно — это то, что идёт на стэк, а другое — в кучу.

stack vs heap

Заблуждение (1): стэк быстрый, а куча большая.

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

Закон: слово heap мы уже искали в стандарте и ничего серьёзного не нашли. Слово stack в основном встречается в параграфах про unsafe-блоки и stackalloc, но мы сейчас не про это.

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

Размер стэка можно поменять для всего бинарника с помощью EDITBIN.EXE /STACK: file.exe

А для каждого отдельного потока — через второй аргумент конструктора new Thread().

По умолчанию, куча действительно больше стэка, но стэк можно увеличить, а куча, хоть и растёт по мере надобности, но не безгранична. Есть куча правил, по которым определяется её размер, так же её можно уменьшить в настройках. Иногда доступная куча оказывается меньше физически доступной памяти Тогда приходится расчехлять unsafe и делать offheap-аллокации.

Вывод: размер и того и другого определяются машиной и рантаймом, но не является определяющим признаком.

Передача по значению / указателю

Заблуждение: значимые типы (структуры) передаются по значению а ссылочные (классы) — по ссылке.

Почему оно популярно? Честно — не знаю. Возможно из курсов Си для начинающих.

Закон:

10.2.5 Value parameters
A parameter declared without a ref or out modifier is a value parameter.

10.2.6 Reference parameters
A parameter declared with a ref modifier is a reference parameter.

10.2.7 Output parameters
A parameter declared with an out modifier is an output parameter.

Думаю, тут всё понятно. То, как передаётся объект, определяется не его типом (ссылочный/значимый) а тем, как объявлен и передан аргумент функции. Добавляется ещё странная вещь под названием Output parameter со своей семантикой, но в реализации это такой же ref-параметр, только требующий инициализации в вызываемом методе. На этом дискуссию можно было бы закончить, но давайте немного займёмся сравнительным языкознанием.

Для сравнения я возьму Java 8-летней давности (в последний раз что-то значимое на джаве я писал примерно тогда, а с тех пор могло что-то поменяться), С++ (а не C, потому что иначе я сам запутаюсь) и C#. Я хотел тут повторить анализ целиком, но просто приведу ссылки: Java, C++, C#.

Краткий пересказ: в Java передача только по значению, но есть ссылочные типы и примитивы (это не совсем значимые типы как в C#, но для сравнения сойдёт) В C++ все типы — значимые, но можно передавать как по значению так и по ссылке (и ещё по указателю/адресу). А в C# сочетаются обе эти семантики: можно взять значимый или ссылочный тип и передать любой из них по ссылке или по значению. Это ортогональные понятия и не надо их смешивать.

P.S.: в новых версиях языка появились in-параметры. С семантической точки зрения они не определяют способ передачи (ведь при запрете изменения объекта нет никакой разницы, как он был передан), но с точки зрения реализации они работают как неизменяемые ref-параметры (readonly ref) и соответственно тоже передаются по ссылке.

P.P.S.: с out-параметрами тоже не всё так просто. Вот в этой статье есть подробный разбор: https://m.habr.com/ru/company/pvs-studio/blog/542210/, рекомендую к прочтению.

string — особенный тип

Заблуждение: ведёт себя как значимый тип, а лежит в куче.

Закон:

9.2.5 The string type
The string type is a sealed class type that inherits directly from object. Instances of the string class represent Unicode character strings.
Values of the string type can be written as string literals (§7.4.5.6).
The keyword string is simply an alias for the predefined class System.String

Как видим, не такой уж он и особенный.

Опыт: ну да, как и все другие классы (ссылочные типы) строки обычно размещаются в куче. Почему говорят, что он ведёт себя как значимый тип? Я много раз такое слышал, но так и не получил чёткого ответа, почему.

Про какие особенности речь?

1. Это неизменяемый (immutable) и запечатанный (sealed) класс. Это значит, что обычными способами нельзя изменить внутри него данные и нельзя от него унаследоваться. Ну и что? Вы можете создавать классы с такими же ограничениями, ничего особенного.

2. Можно сравнивать с помощью оператора==, а обычные структуры нельзя, и для голых классов сравнивается инстанс, но не данные. Ну и что? Для любого своего класса или структуры вы можете написать такой же оператор и они будут вести себя абсолютно так же.

Более того, иммутабельность строк можно нарушить с помощью небольшой щепотки магии. И да, это уже было на хабре.

const vs readonly

Заблуждение:

· const — значение подставляется при компиляции => установить можно только до компиляции

· readonly — установить значение можно только до компиляции или в конструкторе

Закон:

12.20 Constant expressions

A constant expression is an expression that shall be fully evaluated at compile-time

Не «установить значение», а «значение вычисляется» (это тонкое, но важное различие).

15.5.3 Readonly fields

15.5.3.1 General When a field-declaration includes a readonly modifier, the fields introduced by the declaration are readonly fields. Direct assignments to readonly fields can only occur as part of that declaration or in an instance constructor or static constructor in the same class.

Да, вроде бы похоже. Но почему они в разных разделах (12 и 15)? Давайте посмотрим на название: одно — это выражение, а другое — модификатор поля. И это главное их семантическое отличие.

Отсюда вытекает важная деталь реализации:

15.5.3.3 Versioning of constants and static readonly fields
Constants and readonly fields have different binary versioning semantics. When an expression references a constant, the value of the constant is obtained at compile-time, but when an expression references a readonly field, the value of the field is not obtained until run-time.

Значение константы фиксируется на момент компиляции. А статических readonly-полей (которые часто используют как замену) — на этапе выполнения. Если одна сборка зависит от другой, и берёт из неё константы и ридонли-поля, то при их изменении в первой сборке, константы во второй останутся старыми, а readonly-поля подцепятся свежие.

Факт: соберите тестовый проект, и откройте его в декомпиляторе. Вы увидите, что вместо констант встанут их значения (поэтому они и являются выражениями), а для readonly-полей останутся на них ссылки.

Хозяйке на заметку: до .NET Core 3 можно было поменять значение readonly-полей через рефлекшен, начиная же с этой версии такой простой способ больше не работает, но остались другие. Поменять же значения констант, не замаравшись в декомпиляции и рекомпиляции методов, у вас не выйдет.

ref и out

Из статьи-«шпаргалки»: ref и out позволяют внутри метода использовать new и для class и для struct

Факт: см выше (Передача по указателю)

out тоже что ref, только говорит о том что, метод обязательно пересоздаст переменную

Тут, наверное, имелось в виду «переназначит», а не пересоздаст, но сильно придираться не будем.

События, делегаты

Заблуждение:

if (Evt != null) Evt("hello");

Закон: 15.8.2 Field-like events

EventHandler handler = Click; if (handler != null) handler(this, e);

Надеюсь, разницу, объяснять не надо.

P.S.: в новом C# можно не задумываться и писать Evt?.Invoke(«hello»);

Finalizer (destructor) ~

Заблуждение (1): ~Foo() это «деструктор» класса Foo

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

Закон:

15.13 Finalizers
[Note: In an earlier version of this standard, what is now referred to as a «finalizer» was called a
«destructor». Experience has shown that the term «destructor» caused confusion and often resulted to incorrect expectations, especially to programmers knowing C++. In C++, a destructor is called in a determinate manner, whereas, in C#, a finalizer is not. To get determinate behavior from C#, one should use Dispose. end note]

Причина: деструкторы и финализаторы отличаются семантикой, главное отличие — детерминированность.

P.S.: в комментах появилась хорошая историческая справка, спасибо @rstm-sf.

Заблуждение (2): вызывается, когда garbage collector доберется до объекта

Почему популярно? В большинстве простых случаев это действительно именно так и происходит.

Закон:

An instance becomes eligible for finalization when it is no longer possible for any code to use that instance. Execution of the finalizer for the instance may occur at any time after the instance becomes eligible for finalization (§8.9).

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

Факт: финализатор будет вызван у объекта, если он у него есть и объект не побывал в методе SuppressFinalize до начала маркировки объектов на финализацию. Где-то между моментом определения что объект недоступен до момента фактического освобождения памяти. Но это не точно. Отменить финализацию можно с помощью метода GC.SuppressFinalize, хотя это может и не сработать. Более того, рекомендованный шаблон реализации IDisposable именно так и поступает.

Заблуждение (3): вызывается только автоматически средой .Net, нельзя вызвать самостоятельно

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

myObj.GetType().GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Invoke(myObj, null);

Конечно, для симуляции корректного поведения следует пройтись по всей цепочке наследования (ведь так написано в стандарте).

Singleton

Заблуждение: не забудьте про потокобезопасность и lock

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

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

Раньше собеседующий ждал тут что-то типа такого кода:

static Singleton singletonInstance; static readonly Object syncRoot = new Object(); public static Singleton GetInstance()

Это известный паттерн double checked locking, он нужен для ленивой инициализации. А вопрос, напомню, стоял про синглтон.

Более того, этот код тоже имеет проблемы. Дело в том, что модель памяти работает не совсем так, как кажется на первый взгляд (это отдельная большая тема, явно не для собеседований уровня джун/миддл). Чтобы её решить, требуется или вставить volatile в нужном месте или аккуратно использовать MemoryBarrier().

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

public static Singleton Instance < get; >= new Singleton();

Рантайм гарантирует, что это свойство будет потокобезопасно проинициализировано в какой-то момент начиная от запуска и до первого использования. Причём в текущей реализации это делается с достаточной степенью ленивости, так что для 99% случаев такой простой код будет самым безопасным и надёжным. Для любителей чуть большей ленивости есть решение со вложенным классом, но всё же не полной гарантией ленивости.

И, значит, для оставшегося 1% случаев, когда синглтону нужна гарантированная ленивость, мы можем написать:

static readonly Lazy lazy = new Lazy(() => new Singleton()); public static Singleton Instance => lazy.Value;

И да, это тоже уже было на хабре, с кучей комментов.

P.S.: и так совпало, что сегодня же на хабре выложили вот такую подробную статью (18+)!

Вроде, по статье всё. Я явно что-то забыл, или написал неправильно, но для этого есть комменты, да будет срач!

Stack and heap. Структуры данных в .NET

Stack and heap. Структуры данных в .NET

10.09.2018

10379

Рейтинг: 5 . Проголосовало: 2
Вы проголосовали:
Для голосования нужно авторизироваться

advertisement advertisement

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

Разделение памяти

По умолчанию, как только .NET приложение запускается и определяется виртуальный адрес текущего процесса, создаются следующие «кучи»:

Куча для кода — JIT-компилируемый нативный код

Малая объектная куча — объекты до 85 кб

Большая объектная куча — объекты свыше 85 кб*

Куча для обработки данных

*примечание: в случае массивов для данных типа double существует исключение, согласно которому они хранятся в большой объектной куче задолго до достижения размера в 85 кб (double[] считается системой «большим» объектом при достижении размера в 1000 элементов). По отношению к оптимизации 32-битного кода это, конечно, не очень хорошо.

advertisement advertisement

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

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

Впрочем, куча — это не единственная структура данных, которой может похвалиться вселенная .NET. К примеру, есть еще и стек, который крайне полезен для хранения «специфических» типов данных. Сейчас мы рассмотрим в деталях, как устроены эти структуры данных в деталях.

Стек

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

Тема связана со специальностями:

Запоминание порядка выполнения — обращение к стеку

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

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

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

Давайте взглянем на следующий участок кода:

Дабы вызвать Method2, фреймворк должен сохранить адрес конца выполнения метода, которым будет следующая строчка кода (строчка 4 в примере ниже). Этот адрес вместе с параметрами и локальными переменными вызываемого и вызывающего метода хранятся в стеке вызова, как показано на схеме ниже.

Также вы можете увидеть, что происходит, когда Method3 завершает свое выполнение (стек-фрейм покидает стек вызова).

Хранение значимых типов

Также стек используется для хранения переменных любых значимых типов .NET — включая: bool, decimal, int и так далее.

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

Видео курсы по схожей тематике:

C# Starter (ES)

C# Стартовый. Ускоренный курс

C# Стартовый. Ускоренный курс

Может ли экземпляр структуры храниться в куче heap как это сделать

Привет!
Есть пара вопросов про память.
Возник вопрос, почему создатели .NET решили что классы будут создаваться в куче, а структуры в стэке?
Как происходит выделение памяти, если есть

 class A <> struct B < A name get; set;>> var a = new A(); var b = new B

т.е. получается, что для а будет храниться в куче, а b в стэке?

Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
как эта память делится между кучей и стэком?

Re: Stack and Heap

От: adontz http://adontz.wordpress.com/
Дата: 15.06.11 04:36
Оценка:

Здравствуйте, Аноним, Вы писали:

ОС выделяет стек и кучу раздельно для любого процесса (не только .Net).

A journey of a thousand miles must begin with a single step © Lau Tsu
Re: Stack and Heap

От: Lloyd
Дата: 15.06.11 04:51
Оценка:

Здравствуйте, Аноним, Вы писали:

А>Возник вопрос, почему создатели .NET решили что классы будут создаваться в куче, а структуры в стэке?
А>Как происходит выделение памяти, если есть

А>

 А>class A <> А>struct B < A name get; set;>> А>var a = new A(); А>var b = new B < name = a;>А>

А>т.е. получается, что для а будет храниться в куче, а b в стэке?

Не совсем. Переменная а хранится на стеке, но поле name-а будет храниться в куче, в том же куске памяти, что и экземпляр класса B.

А>Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
А>как эта память делится между кучей и стэком?

Из этого куска по умолчанию каждый поток программы получает 1 мб под стек. Остальное может испольноваться под выделение объектов.

Re[2]: Stack and Heap

От: Аноним
Дата: 15.06.11 05:05
Оценка:

Здравствуйте, Lloyd, Вы писали:

L>Здравствуйте, Аноним, Вы писали:

А>>Возник вопрос, почему создатели .NET решили что классы будут создаваться в куче, а структуры в стэке?
А>>Как происходит выделение памяти, если есть

А>>

 А>>class A <> А>>struct B < A name get; set;>> А>>var a = new A(); А>>var b = new B < name = a;>А>>

А>>т.е. получается, что для а будет храниться в куче, а b в стэке?

L>Не совсем. Переменная а хранится на стеке, но поле name-а будет храниться в куче, в том же куске памяти, что и экземпляр класса B.

вы наверное описались. Переменная а хранится в куче, но поле name-а будет храниться в стэке, в том же куске памяти, что и экземпляр структуры B.

т.е. получается, что память под a будет выделена дважды: в куче для а и на стэке для name?
а name хранит ссылку на a?
хочется понять как это всё выглядит на уровне адресов или картинок

А>>Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
А>>как эта память делится между кучей и стэком?

L>Из этого куска по умолчанию каждый поток программы получает 1 мб под стек. Остальное может испольноваться под выделение объектов.

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

Re[3]: Stack and Heap

От: adontz http://adontz.wordpress.com/
Дата: 15.06.11 05:09
Оценка:

Здравствуйте, Аноним, Вы писали:

А>вы наверное описались. Переменная а хранится в куче, но поле name-а будет храниться в стэке, в том же куске памяти, что и экземпляр структуры B.

A journey of a thousand miles must begin with a single step © Lau Tsu
Re[2]: Stack and Heap

От: Аноним
Дата: 15.06.11 05:12
Оценка:

Здравствуйте, adontz, Вы писали:

A>Здравствуйте, Аноним, Вы писали:

A>ОС выделяет стек и кучу раздельно для любого процесса (не только .Net).

не понял, что значит раздельно. внутри 2 гигов памяти для процесса, эта память как-то делится на «память для стэка» и «память для кучи»?
т.е

Re: Stack and Heap

От: мыщъх http://nezumi-lab.org
Дата: 15.06.11 05:16
Оценка: -1

Здравствуйте, Аноним, Вы писали:

А>Привет!
А>Есть пара вопросов про память.
А>Возник вопрос, почему создатели .NET решили что классы будут создаваться в куче, а структуры в стэке?
кучи много, стека мало (так заведено), но со стеком нет проблем освобождения ресурсов и фрагментации. можно еще назвать много причин, но структуры могут так же хранится и в куче. так что вы тут напутали все.

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

А>Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
А>как эта память делится между кучей и стэком?
вообще-то стек тоже часть кучи и в куче можно найти блок памяти, отведенный под стек.

americans fought a war for a freedom. another one to end slavery. so, what do some of them choose to do with their freedom? become slaves.

Re[3]: Stack and Heap

От: adontz http://adontz.wordpress.com/
Дата: 15.06.11 05:17
Оценка:

Здравствуйте, Аноним, Вы писали:

A>>ОС выделяет стек и кучу раздельно для любого процесса (не только .Net).
А>не понял, что значит раздельно. внутри 2 гигов памяти для процесса, эта память как-то делится на «память для стэка» и «память для кучи»?

Да, эта память делится, ОС явно размечает память для стека и память для кучи. Это просто адреса, запись и чтение в рамках одного процесса никак не запрещаются.

A journey of a thousand miles must begin with a single step © Lau Tsu
Re[2]: Stack and Heap

От: Аноним
Дата: 15.06.11 05:30
Оценка:

Здравствуйте, мыщъх, Вы писали:

М>Здравствуйте, Аноним, Вы писали:

А>>Привет!
А>>Есть пара вопросов про память.
А>>Возник вопрос, почему создатели .NET решили что классы будут создаваться в куче, а структуры в стэке?
М>кучи много, стека мало (так заведено), но со стеком нет проблем освобождения ресурсов и фрагментации.

с фрагментацией понятно, что ее нет в стеке, в отличие от кучи, а про освобождение ресурсов можно пример

М>можно еще назвать много причин, но структуры могут так же хранится и в куче. так что вы тут напутали все.

М>’new’ он устроен так, что выделяет память из кучи. а когда вы просто объявляете переменную, то она выделяется на стеке.

это как «объявляете переменную»?

class A <> struct B < A name get; set;>> var a = new A(); var b = new B

как здесь память будет выделяться?

А>>Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
А>>как эта память делится между кучей и стэком?
М>вообще-то стек тоже часть кучи и в куче можно найти блок памяти, отведенный под стек.

что-то непонятно. adontz говорит

Автор: adontz
Дата: 15.06.11

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

Re: Stack and Heap

От: Sinix
Дата: 15.06.11 05:40
Оценка:

Здравствуйте, Аноним, Вы писали:

А>т.е. получается, что для а будет храниться в куче, а b в стэке?
Стек в il — это абстракция, в реальности значения на стеке спокойно могут быть аллоцированы в стеке процессора или в его регистрах.

В примере выше на стеке будут заняты 2 ячейки.

В 1й — указатель на экземпляр типа A, аллоцированный в куче: метаданные (SyncBlck + TypeHandle, 8(x86)/16(x64) байт) и поля экземпляра — указатели и заинлайненные структуры. Поскольку полей у A нет, экземпляр просто отгребёт себе 8-16 байт в куче.

Во-второй — заинлайненная структура. Никаких метаданных, только поле name — указатель на всё тот же экземпляр A.

Re[2]: Stack and Heap

От: Аноним
Дата: 15.06.11 05:51
Оценка:

Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, Аноним, Вы писали:

А>>т.е. получается, что для а будет храниться в куче, а b в стэке?
S>Стек в il — это абстракция, в реальности значения на стеке спокойно могут быть аллоцированы в стеке процессора или в его регистрах.

S>В примере выше на стеке будут заняты 2 ячейки.

S>В 1й — указатель на экземпляр типа A, аллоцированный в куче: метаданные (SyncBlck + TypeHandle, 8(x86)/16(x64) байт) и поля экземпляра — указатели и заинлайненные структуры. Поскольку полей у A нет, экземпляр просто отгребёт себе 8-16 байт в куче.

а что именно занимает 8-16 байт при создании экземпляра класса без полей?

Re[3]: Stack and Heap

От: Lloyd
Дата: 15.06.11 06:24
Оценка:

Здравствуйте, Аноним, Вы писали:

А>>>т.е. получается, что для а будет храниться в куче, а b в стэке?

L>>Не совсем. Переменная а хранится на стеке, но поле name-а будет храниться в куче, в том же куске памяти, что и экземпляр класса B.

А>вы наверное описались.

Так и есть, описался. Почему-то подумал, что A — стрктур.

А>Переменная а хранится в куче,

Экземпляр будет храниться в куче, а переменная (ссылка)- на стеке.

А>но поле name-а будет храниться в стэке, в том же куске памяти, что и экземпляр структуры B.

Да

А>т.е. получается, что память под a будет выделена дважды: в куче для а и на стэке для name?

а — просто указатель на экземпляр класса, выделенный в куче. Сама ссылка будет лежать в составе структуры B, экземпляр — в куче.

А>а name хранит ссылку на a?

И a, и name хранят ссылку на экземпляр класса в куче.

А>хочется понять как это всё выглядит на уровне адресов или картинок

Ну это не ко мне. Из меня художник никакой.

А>>>Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
А>>>как эта память делится между кучей и стэком?

L>>Из этого куска по умолчанию каждый поток программы получает 1 мб под стек. Остальное может испольноваться под выделение объектов.

А>т.е. если я знаю что у меня будет создаваться много объектов, то лучше использовать классы?

Зависит от времени жизни и размера объектов. Погугли, есть официальная рекомендация когда стоит делать структуры, а когда — классы.

Re[3]: Stack and Heap

От: мыщъх http://nezumi-lab.org
Дата: 15.06.11 06:26
Оценка: -2

Здравствуйте, Аноним, Вы писали:

А>Здравствуйте, мыщъх, Вы писали:

М>>Здравствуйте, Аноним, Вы писали:

А>>>Привет!
А>>>Есть пара вопросов про память.
А>>>Возник вопрос, почему создатели .NET решили что классы будут создаваться в куче, а структуры в стэке?
М>>кучи много, стека мало (так заведено), но со стеком нет проблем освобождения ресурсов и фрагментации.
А>с фрагментацией понятно, что ее нет в стеке, в отличие от кучи, а про освобождение ресурсов можно пример
когда память выделяется на куче, то ее нужно освобождать. а гарантировать освобождение не так-то просто и обычно оно далеко не бесплатно в смысле процессорных ресурсов. выделение памяти на стеке — одна инструкция ЦП. освобождение — еще одна инструкция. освобождение ресурсов на куче это в среднем тысячи инструкций ЦП с кучей обращений к памяти.

М>>можно еще назвать много причин, но структуры могут так же хранится и в куче. так что вы тут напутали все.
М>>’new’ он устроен так, что выделяет память из кучи. а когда вы просто объявляете переменную, то она выделяется на стеке.
А>это как «объявляете переменную»?
вот var a; это объявление переменной, она на стеке. a = new obj; это выделение памяти в куче под объект obj, вызов его конструктора (в дотнете есть конструкторы? не знаток данной платформы) и занесение указателя на obj в переменную a.

class A a; —> тут мы без new запихали класс в стек. struct B b; —> аналогично. b = new B —> структура в куче, переменная b на стеке и в ней указатель на структуру.

А>class A <> А>struct B < A name get; set;>> А>var a = new A(); А>var b = new B < name = a;>А>

А>как здесь память будет выделяться?

при объявлении класса и структуры (две первых строки) ничего нигде не выделяется.
a, b — на стеке. хранят указатели на класс и структуру, выделенную из кучи по ‘new’.

А>что-то непонятно. adontz говорит

Автор: adontz
Дата: 15.06.11

что память для кучи и стэка в рамках процесса живут раздельно,
А> а вы говорите, что стэк является частью кучи. Что не так я понял?
тут, вероятно, путаница в терминологии. но просматривая блоки динамической памяти средствами API винды в них можно обнаружить и стек. и чем больше потоков мы создадим, тем больше ранее свободных блоков будет откушано. и потому куча будет таять буквально на глазах. но тут есть тонкость. дотнет и ms vc с виндовой кучей непосредственно не работает. у среды есть свой хип-манагер, опирающийся на системный. это потому что системный не оптимизирован под маленькие блоки. и с этой точки зрения — куча приложения на ЯВУ и стек живут раздельно, но, как ни крути, адресное пространство у них одно. положим, сейчас у меня есть 1,5 гига кучи. но если я создам 100 потоков и каждому выделю по 10 метров стека, то, очевидно, полутора гектар кучи у меня уже не будет. более того, переполнение кучи может привести к тому, что мы залезем в чей-то стек (для этого правда нужно создать много потоков, т.к. стек первичного потока более или менее защищен от посягательств со стороны кучи).

а вообще в карте памяти все видно как там оно происходит на самом деле.

americans fought a war for a freedom. another one to end slavery. so, what do some of them choose to do with their freedom? become slaves.

Re[2]: Stack and Heap

От: Lloyd
Дата: 15.06.11 06:27
Оценка:

Здравствуйте, Sinix, Вы писали:

А>>т.е. получается, что для а будет храниться в куче, а b в стэке?
S>Стек в il — это абстракция, в реальности значения на стеке спокойно могут быть аллоцированы в стеке процессора или в его регистрах.

Что такое «стек процессора»?

Re[2]: Stack and Heap

От: Воронков Василий
Дата: 15.06.11 06:34
Оценка:

Здравствуйте, мыщъх, Вы писали:

М>’new’ он устроен так, что выделяет память из кучи. а когда вы просто объявляете переменную, то она выделяется на стеке. это в дотнете. в других языках стековых переменных может и не быть. совсем.

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

А>>Также интересно как память процесса делится на стэк и кучу? т.е. выделила ОС процессу 4 гига. 2 гига непосредственно под память.
А>>как эта память делится между кучей и стэком?
М>вообще-то стек тоже часть кучи и в куче можно найти блок памяти, отведенный под стек.

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

Re[2]: Stack and Heap

От: Воронков Василий
Дата: 15.06.11 06:39
Оценка:

Здравствуйте, Sinix, Вы писали:

А>>т.е. получается, что для а будет храниться в куче, а b в стэке?
S>Стек в il — это абстракция, в реальности значения на стеке спокойно могут быть аллоцированы в стеке процессора или в его регистрах.

А что, другие языки не умеют значения в регистры писать? Этак можно сказать, что стек всегда абстракция.
В реальности нет там никаких абстракций, вполне реальная штука. Однако код может быть оптимизирован или, наоборот, «пессимизирован» — если он, к примеру, попадает в замыкание и все как бы локальные переменные в действительности вытаскиваются в кучу.

S>В примере выше на стеке будут заняты 2 ячейки.

Что такое «ячейки»? Выравнивание по умолчанию что ли? 4 или 8 байт?

Re[4]: Stack and Heap

От: HowardLovekraft
Дата: 15.06.11 07:16
Оценка:

Здравствуйте, мыщъх, Вы писали:

М>b = new B —> структура в куче, переменная b на стеке и в ней указатель на структуру.
Может быть, пора остановить поток сознания?

Re[3]: Stack and Heap

От: Sinix
Дата: 15.06.11 11:17
Оценка:

Здравствуйте, Lloyd, Вы писали:

L>Что такое «стек процессора»?
Ок, неудачно выразился. «Храниться напрямую в регистрах или вытаскиваиться через push/pop» — так пойдёт?

Re[3]: Stack and Heap

От: Sinix
Дата: 15.06.11 11:35
Оценка:

Здравствуйте, Воронков Василий, Вы писали:

S>>Стек в il — это абстракция, в реальности значения на стеке спокойно могут быть аллоцированы в стеке процессора или в его регистрах.
ВВ>А что, другие языки не умеют значения в регистры писать? Этак можно сказать, что стек всегда абстракция.
Умеют. Стек в il введён ради стековой модели вычислений и не имеет никакого отношения к результату jit-a. Пока мы не опускаемся до ньюансов реализации конкретного рантайма (а по MS CLR открытых материалов практически нет), нет никакого смысла говорить о стеке/регистрах.
Подробней — тынц и тынц.

S>>В примере выше на стеке будут заняты 2 ячейки.
ВВ>Что такое «ячейки»?
Абстрактная фигня, не имеющая никакого отношения к реальности. В il будет ldloc.0;ldloc.1; Размер зависит от того. что кладётся на стек. Для bool,byte и прочей мелочи — 4 байта, для всего остального — чтоб влезло. Но это, опять-таки, implementation details.

Re[4]: Stack and Heap

От: Аноним
Дата: 15.06.11 11:39
Оценка:

Здравствуйте, Lloyd, Вы писали:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *