Подпишитесь на мой телеграм-канал, там я пишу о дотнете и веб-разработке.

Друзья:
//devdigest platform - новости и полезные статьи о дотнете.

Как .NET хранит объекты в памяти

July 06, 2013

Причиной многих заблуждений относительно того как .NET хранит объекты в памяти, является ошибочное представление, что значимые типы всегда хранятся в стеке (stack), а ссылочные в куче (heap). На самом деле ссылочные типы - всегда хранятся в куче, а вот значимые типы могут храниться как в куче, так и в стеке.

Все локальные переменные (local variables) и параметры метода хранятся в стеке. Это касается переменных и параметров и значимых (value types) и ссылочных типов (reference types). Разница между ними в том, что они хранят. Переменные и параметры значимых типов - хранят само значение переменной. А переменные и параметры ссылочных типов - хранят ссылку на объект расположенный в куче.

Теперь рассмотрим поля (fields).

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

Рассмотрим на примере следующих двух типов:

class RefType {
    public int I;
    public string S;
public long L;
}

struct ValType {
    public int I;
    public string S;
    public long L;
}

Экземпляр каждого из этих типов будет занимать 16 байт (в 32 битной программе).

  • Поле I занимает 4 байта
  • Поле S занимает 4 байта - ссылка на объект в куче
  • Поле L занимает 8 байт

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

Так что память выделенная под объекты обоих типов выглядит так:

 0 ┌───────────────────┐
   │        I          │
 4 ├───────────────────┤
   │        S          │
 8 ├───────────────────┤
   │        L          │
   │                   │
16 └───────────────────┘

Теперь представим, что наша функция содержит три локальных переменных:

RefType refType;
ValType valType;
int[]   intArray;

Тогда память в стеке выделенная под эту функцию будет выглядеть следующим образом:

 0 ┌───────────────────┐
   │	 refType       |
 4 ├───────────────────┤
   │     valType       │
   │                   │
   │                   │
   │                   │
20 ├───────────────────┤
   │	 intArray      │
24 └───────────────────┘

Теперь инициализируем эти переменные значениями:

refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;

valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;

intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;

Тогда память в стеке примет следующее состояние:

 0 ┌───────────────────┐
   │    0x4A963B68     │ -- адрес в куче объекта `refType`
 4 ├───────────────────┤
   │       200         │ -- значение `valType.I`
   │    0x4A984C10     │ -- адрес в куче объекта `valType.S`
   │    0x44556677     │ -- нижние 32-бита `valType.L`
   │    0x00112233     │ -- верхние 32-бита `valType.L`
20 ├───────────────────┤
   │    0x4AA4C288     │ -- адрес в куче объекта `intArray`
24 └───────────────────┘

Теперь давайте посмотрим, что будет храниться в памяти по адресу 0x4A963B68 (значение объекта refType):

 0 ┌───────────────────┐
   │       100         │ -- значение `refType.I`
 4 ├───────────────────┤
   │    0x4A984D88     │ -- адрес в куче объекта  `refType.S`
 8 ├───────────────────┤
   │    0x89ABCDEF     │ -- нижние 32-бита `refType.L`
   │    0x01234567     │ -- верхние 32-бита `refType.L`
16 └───────────────────┘

И наконец посмотрим, что хранится в куче по адресу 0x4AA4C288 (массив intArray):

 0 ┌───────────────────┐
   │        4          │ -- длина массива
 4 ├───────────────────┤
   │       300         │ -- `intArray[0]`
 8 ├───────────────────┤
   │       301         │ -- `intArray[1]`
12 ├───────────────────┤
   │       302         │ -- `intArray[2]`
16 ├───────────────────┤
   │       303         │ -- `intArray[3]`
20 └───────────────────┘

Таким образом при поведении по-умолчанию:

  • Место под локальные переменные и параметры метода всегда выделяется в стеке
  • Локальные переменные и параметры значимого типа - всегда содержат значение переменной
  • Локальные переменные и параметры ссылочного типа - всегда содержат ссылку на место в куче
  • Место под поля может выделяться как в стеке, так и в куче в зависимости от того где выделяется память под объект, который содержит это поле.
  • Поле значимого типа - всегда содержит значение
  • Поле ссылочного типа - всегда содержит ссылку на адрес в куче
  • Место под массив всегда выделяется в куче