[Перевод] Знакомство с внутренним устройством .NET Framework. Посмотрим, как CLR создаёт объекты
Вниманию читателей «Хабрахабра» сообщества представляется перевод статьи Хану Коммалапати и Тома Кристиана об внутреннем устройстве .NET. Пока готовился перевод, оказалось что статья уже переведена на сайте Microsoft. Какой лучше решать читателям «Хабрахабра».
В статье рассматривается:
- Системный домен (SystemDomain), Домен общего пользования (SharedDomain) и домен по умолчанию (DefaultDomain)
- Представление объекта и другие особенности организации памяти
- Представление таблицы методов
- Распределение методов
Используемые технологии: .NET Framework, C#
- Домены создаваемые начальным загрузчиком
- Системный домен
- Домен общего пользования (разделяемый)
- Дефолтный домен
- Загрузчик куч
- Основы типов
- Экземпляр объекта
- Таблица методов
- Размер базового экземпляра
- Таблица слотов метода
- Описатель метода
- Карта таблиц виртуальных методов интерфейсов и карта интерфейса
- Виртуальное распределение
- Статические переменные
- EEClass
- Заключение
Общая среда исполнения (CLR) становится (или уже стала) главной инфраструктурой для построения приложений в Windows, поэтому наличие глубокого понимания его внутреннего устройства поможет создавать эффективные приложения промышленного класса.
В этой статье, мы будем исследовать внутреннее устройство CLR, включая макет экземпляра объекта, макет таблицы методов, распределение методов, интерфейсное распределение, а также различные структуры данных.
Мы будем использовать очень простые фрагменты С# кода, любое неявное использование синтаксиса языка программирования подразумевает С#. Некоторые обсуждаемые структуры данных и алгоритмы будут изменены в следующих версиях среды Microsoft® .NET Framework, но концептуальные основы останутся прежними. Будем использовать отладчик Visual Studio® .NET 2003 и расширение для отладки Son of Strike (SOS) чтобы просматривать структуры данных обсуждаемые в статье. SOS загружает внутренние данные CLR, и позволяет просматривать, сохранять интересующую информацию. Посмотрите процедуру загрузки SOS.dll в процесс отладчика в соответствующих источниках.
See the «Son of Strike» sidebar for loading SOS.dll into the Visual Studio .NET 2003 debugger process.
В статье, мы будем описывать классы соответствующие реализациям в Shared Source CLI (SSCLI).
Таблица на рисунке 1 поможет в исследовании мегабайтов кода в SSCLI во время поиска необходимых структур.
Компонент | SSCLI Путь |
---|---|
AppDomain | /sscli/clr/src/vm/appdomain.hpp |
AppDomainStringLiteralMap | /sscli/clr/src/vm/stringliteralmap.h |
BaseDomain | /sscli/clr/src/vm/appdomain.hpp |
ClassLoader | /sscli/clr/src/vm/clsload.hpp |
EEClass | /sscli/clr/src/vm/class.h |
FieldDescs | /sscli/clr/src/vm/field.h |
GCHeap | /sscli/clr/src/vm/gc.h |
GlobalStringLiteralMap | /sscli/clr/src/vm/stringliteralmap.h |
HandleTable | /sscli/clr/src/vm/handletable.h |
InterfaceVTableMapMgr | /sscli/clr/src/vm/appdomain.hpp |
Large Object Heap | /sscli/clr/src/vm/gc.h |
LayoutKind | /sscli/clr/src/bcl/system/runtime/interopservices/layoutkind.cs |
LoaderHeaps | /sscli/clr/src/inc/utilcode.h |
MethodDescs | /sscli/clr/src/vm/method.hpp |
MethodTables | /sscli/clr/src/vm/class.h |
OBJECTREF | /sscli/clr/src/vm/typehandle.h |
SecurityContext | /sscli/clr/src/vm/security.h |
SecurityDescriptor | /sscli/clr/src/vm/security.h |
SharedDomain | /sscli/clr/src/vm/appdomain.hpp |
StructLayoutAttribute | /sscli/clr/src/bcl/system/runtime/interopservices/attributes.cs |
SyncTableEntry | /sscli/clr/src/vm/syncblk.h |
System namespace | /sscli/clr/src/bcl/system |
SystemDomain | /sscli/clr/src/vm/appdomain.hpp |
TypeHandle | /sscli/clr/src/vm/typehandle.h |
Момент на который стоит обратить внимание перед тем как мы пойдём дальше — информация предоставленная в этой статье действительна, только для .NET Framework 1.1 (также в основном это соответствует Shared Source CLI 1.0, с учётом ряда заметных исключений, присутствующих в различных сценариях взаимодействия) при исполнении на платформе x86. Информация изменена в следующих версиях .NET Framework, поэтому пожалуйста не занимайтесь сборкой ваших приложений с абсолютными ссылками на эти внутренние структуры.
Домены создаваемые загрузчиком CLR
Перед тем как запустить первую строчку управляемого кода, создаётся три домена приложения. Два из них не доступны в управляемом коде и даже не видимы для хоста CLR. Они могут быть только созданы только при загрузке CLR обеспечиваемой шиной mscoree.dll и mscorwks.dll (или mscorsvr.dll для мультипроцессорных систем). Как вы можете видеть на рисунке 2, это системный домен и разделяемый (общий) домен, они могут существовать только в одном экеземпляре. Третий домен — дефолтный домен, только экземпляр этого AppDomain имеем наименование. Для простого хоста CLR, такого как консольное приложение, имя дефолтного домена приложений содержит имя исполняемого образа. Дополнительные домены, могут быть созданы из управляемого кода методом AppDomain.CreateDomain или из хоста неуправляемого кода используя интерфейс ICORRuntimeHost.
Сложные хосты, такие как ASP.NET создают необходимое количество доменов, в соответствии с количеством приложений, работающих в обслуживаемом Web сайте.
Рисунок 2 Домены созданные загрузчиком CLR
Системный домен
Системный домен создаёт и инициализирует домен общего доступа (SharedDomain) и домен по умолчанию (Default). Он же выполняет загрузку системной библиотеки mscorlib.dll в домен общего доступа.
Также системный домен содержит доступные в границах процесса строковые константы, интернированные явно или не явно.
Интернирование строк это функционал оптимизации, немного тоталитарный в среде .NET Framework 1.1, так как CLR не даёт возможности сборкам оптимизировать этот фунционал. При этом, память используется для хранения только одного экземпляра строки для всех строковых литералов во всех доменах приложения.
Системный домен также служит для генерации идентификаторов интерфейсов в границах процесса, которые используются при создании карты интерфейсов (InterfaceVtableMaps) в каждом домене приложений (AppDomain)
Системный домен прослеживает все домены в процессе и предоставляет функциональность загрузки и выгрузки доменов приложений.
Домен общего доступа (SharedDomain)
Весь доменно-нейтральный код загружается в домен общего доступа. Mscorlib, системная библиотека, необходима для кода пользователя во всех доменах приложений (AppDomains). Эта бибилиотека автоматически загружается в домен общего доступа. Базовые типа из пространства имён System, такие как Object, ValueType, Array, Enum, String и Delegate загружаются предварительно в этот домен в процессе загрузки CLR загрузчиком. Код пользователя может также быть загружен в этот домен, с помощью установки атрибутов LoaderOptimization приложением хоcтом CLR во время вызова CorBindToRuntimeEx. Консольное приложение может загружать код в домен общего доступа при добавлении атрибута System.LoaderOptimizationAttribute к методу Main приложения. Домен общего доступа также управляет картой сборок, индексированной относительно базового адреса, карта действует как таблица справочник для управления общими зависимостями сборок, загруженных в дефолтный домен и другие домены приложений, созданные в управляемом коде. Дефолтный домен служит только для загрузки частного кода пользователя, который не должен быть доступен другим приложениям.
Дефолтный домен
Дефолтный домен это экземпляр домена приложений, где как правило выполняется код приложения. В то время как некоторые приложения требуют, чтобы дополнительные домены приложения были созданы во время выполнения (такие что имеют архитектуру плагинов или приложения выполняющие генерацию значительного количества кода во время выполнения), большинство приложений создают один домен в течение их выполнения. Весь код, выполняемый в этом домене контекстно ограничен на уровне домена. Если в приложении созданы несколько доменов приложения, любой кросс-доменный доступ будет происходить через прокси .NET Remoting. Дополнительные внутри-доменные границы могут быть созданы используя типы наследованные от System.ContextBoundObject.
Каждый домен приложений имеет свои собственные SecurityDescriptor, SecurityContext и DefaultContext, также как собственный загрузчик куч (High-Frequency Heap, Low-Frequency Heap, and Stub Heap),
Таблицы описателей (Handle Table, Large Object Heap Handle Table), Менеджер карты интерейсов Vtable и кэш сборок.
Кучи загрузчика
Кучи загрузчика (LoaderHeaps) предназначены для загрузки различных артефактов времени выполнения CLR и артефактов оптимизации, существующих в течение всего времени существования домена. Эти кучи увеличиваются на предсказуемые фрагменты для минимизации фрагментации. Кучи загрузчика отличаются от кучи сборщика мусора (GC) (или набора куч в случае симметричных мультипроцессоров SMP) в том что куча сборщика мусора содержит экземпляры объектов, а кучи загрузчика содержат системные типы. Часто запрашиваемые артефакты, такие как таблицы методов, описатели методов (MethodDescs), описатели полей (FieldDescs) и карта интерфейсов располагаются в куче частого доступа (HighFrequencyHeap). Структуры, к которым обращения более редки, такие как EEClass и загрузчик классов (ClassLoader), а также их служебные таблицы, располагаются в куче с низкой частотой обращений (LowFrequencyHeap). Служебная куча (StubHeap) содержит блоки, обеспечивающие поддержку безопасности доступа в коде code access security (CAS), оболочку COM вызовов и вызовов P/Invoke. Рассмотрев домены и кучи загрузчики на высоком уровне, теперь посмотрим на их физическую организация боле пристально в контексте простого приложения на рисунке 3. Остановим выполнение программы на «mc.Method1();» и создадим дамп домена с помощью расширенной команды DumpDomain отладчика SOS. Ниже представлен результат:
!DumpDomain
System Domain: 793e9d58, LowFrequencyHeap: 793e9dbc,
HighFrequencyHeap: 793e9e14, StubHeap: 793e9e6c,
Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40
Shared Domain: 793eb278, LowFrequencyHeap: 793eb2dc,
HighFrequencyHeap: 793eb334, StubHeap: 793eb38c,
Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40
Domain 1: 149100, LowFrequencyHeap: 00149164,
HighFrequencyHeap: 001491bc, StubHeap: 00149214,
Name: Sample1.exe, Assembly: 00164938 [Sample1],
ClassLoader: 00164a78
using System;
public interface MyInterface1
{
void Method1();
void Method2();
}
public interface MyInterface2
{
void Method2();
void Method3();
}
class MyClass : MyInterface1, MyInterface2
{
public static string str = "MyString";
public static uint ui = 0xAAAAAAAA;
public void Method1() { Console.WriteLine("Method1"); }
public void Method2() { Console.WriteLine("Method2"); }
public virtual void Method3() { Console.WriteLine("Method3"); }
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
MyInterface1 mi1 = mc;
MyInterface2 mi2 = mc;
int i = MyClass.str.Length;
uint j = MyClass.ui;
mc.Method1();
mi1.Method1();
mi1.Method2();
mi2.Method2();
mi2.Method3();
mc.Method3();
}
}
Наше консольное приложение, Sample1.exe, загружено в домен приложения (AppDomain), который имеет имя «Sample1.exe». Mscorlib.dll загружен в домен общего доступа (SharedDomain), но также фигурирует в системном домене (SystemDomain), как системная библиотека ядра. Куча высокочастотного доступа (HighFrequencyHeap), низкочастотного доступа (LowFrequencyHeap) и stub-куча (StubHeap) располагаются в каждом домене. Системный домен и домен общего доступа используют один и тот же загрузчик классов (ClassLoader), в то время как дефолтный домен (Default AppDomain) использует свой собственный.
Результат команды не отображает зарезервированный и используемый размер куч загрузчика. Куча высокочастотного доступа первоначально резервирует 32Кб и использует 4Кб.
Куча низкочастотного доступа stub кучи первоначально резервируют 8Кб и занимают 4Кб.
Также не показана куча карты интерфейсов (InterfaceVtableMap, далее IVMap) Каждый домен обладает картой интерфейсов, которая создаётся на своей собственной куче загрузчика во время фазы инициализации домена. Куча карты интерфейсов (IVMap) резервирует 4Кб и занимает 4Кб первоначально. Мы обсудим значимость карты интерфейсов, когда будем исследовать макет типа (type layout) в последующих секциях.
На Рисуноке 2 показаны куча дефолтного процесса (default Process Heap), куча компилятора времени выполнения (JIT Code), куча сборщика мусора (GC) для маленьких объектов (SOH) и куча больших объектов (LOH) (для объектов с размером 85000 байт или более) чтобы проиллюстрировать семантическое различие между ними и кучами загрузчика. Джиттер или компилятор времени выполнения генерирует инструкции для архитектуры x86 и сохраняет их в куч для JIT кода. Куча сборщика мусора и куча больших объектов являются кучами, которые обрабатываются сборщиком мусора, на этих кучах создаются управляемые объекты.
Основы типов
Тип является фундаментальным элементом программирования в .NET. В C# тип может быть объявлен с помощью следующих ключевых слов: class, struct и interface. Большинство типов создаются программистом явно, однако, в особенных случаях взаимодействия и в сценариях вызовов удалённых объектов (.NET Remoting), .NET CLR
Most types are explicitly created by the programmer, however, in special interoperability cases and remote object invocation (.NET Remoting) scenarios, the .NET CLR генерирует типы неявно. Эти генерируемые типы включают COM и вызываемые обертки времени выполнения (Runtime Callable Wrappers) и сквозные прокси (Transparent Proxies).
Мы исследуем .NET фундаментальные типы, начиная со структуры стека что содержит ссылки на объект (как правило, стек — одно из мест, с которых экземпляр объекта начинает своё сущесвование).
Код приведённый на Рисунке 4 содержит простую программу с консольной точкой входа, где вызывается статический метод.
Метод Method1создаёт экземпляр типа SmallClass, который содержит массив байт используемый для демонстрации создания экземпляра объекта в куче больших объектов LOH. Код тривиален, но будет задействован в нашем обсуждении.
using System;
class SmallClass
{
private byte[] _largeObj;
public SmallClass(int size)
{
_largeObj = new byte[size];
_largeObj[0] = 0xAA;
_largeObj[1] = 0xBB;
_largeObj[2] = 0xCC;
}
public byte[] LargeObj
{
get { return this._largeObj; }
}
}
class SimpleProgram
{
static void Main(string[] args)
{
SmallClass smallObj = SimpleProgram.Create(84930,10,15,20,25);
return;
}
static SmallClass Create(int size1, int size2, int size3,
int size4, int size5)
{
int objSize = size1 + size2 + size3 + size4 + size5;
SmallClass smallObj = new SmallClass(objSize);
return smallObj;
}
}
Рисунок 5 показывает снимок типичного стека вызовов fastcall остановленного в точке останова на строке «return smallObj;» в методе Create. (Fastcall — .NET конвенция вызовов которая определяет, что аргументы передаются в функции в регистрах, когда это возможно, с остальными аргументами передаваемыми через стек справа на лево и затем извлекаемыми из стека вызываемой функцией
Локальная переменная значимого типа или типа-значения objSize размещена прямо в стеке. Переменные ссылочного типа, такие как smallObj хранятся с фиксированным занимаемым размером (4 битовое двойное слово DWORD) в стеке и содержат адрес экземпляров объектов размещённых в обычной куче сборщика мусора.
В традиционном C++, this — это указатель на объект; в управляемом мире программирования this — это референс или ссылка на объект (object reference). Тем не менее, она содержит адрес экземпляра объекта. Мы будем использовать термин экземпляр объекта (ObjectInstance) для структуры данных расположенной на адресе указанном в ссылке на объект.
Рисунок 5 SimpleProgram стек и кучи
Экземпляр объекта smallObj на обычной куче сборщика мусора содержит Byte[] указывающий на _largeObj, чей размер 85000 байт (заметьте, что рисунок показывает 85016 байт, что является действительным размером занимаемой области). CLR обращается с объектами размером более чем или равному 85000 байт по другому, в отличие от объектов меньшего. Большие объекты располагаются в куче больших объектов (LOH), в то время как маленькте объекты создаются, а обычной куче сборщика мусора, которая оптимизирует размещение объектов и сбор мусора. LOH не сжимается, при этом обычная куча сжимается при каждом сборе мусора. Более того LOH очищается только при полном сборе мусора.
Экземпляр smallObj содержит описатель типа указывающий на таблицу методов (MethodTable) соответствующего типа. Будет присутствовать по одной таблице методов для каждого объявленного и все экземпляры объектов одного и того же типа будут указывать на одну и ту же таблицу методов. Также описатель будет содержать информацию о разновидности типа (интерфейс, абстрактный класс, конкретный класс, обёртка COM, прокси), число реализованных интерфейсов, карту интерфейсов для распределения методов, число слотов в таблице методов и таблицу слотов указывающих на реализацию.
Одна важная структура данных указывает на EEClass. Загрузчик классов CLR создаёт EEClass из метаданных до того как формируется таблица методов. На Рисунке 4, таблица методов SmallClass указывает на его EEClass. Эти структуры указывают на их модули и сборки. Таблица методов и EEClass как правило располагаются в домен-спeцифичных кучах загрузчика. Byte[] — это особый случай; Таблица методов и EEClass располагаются в кучах загрузчика домена общего доступа. Кучи загрузчика относятся к определённому домену (домен-специфичны) и любые структуры данных, упомянутые ранее, однажды загруженные, никуда не денутся пока домен не будет выгружен. Также, дефолтный домен не может быть выгружен и следовательно код существует пока не будет остановлен CLR.
Экземпляр объекта
Как мы заметили, все экземпляры типов-значений либо встраиваются в стек потока или встраиваются в кучу сборщика мусора. Все ссылочные типы создаются на куче сборщика мусора или куче больших объектов (LOH). Рисунок 6 показывает типичный макет экземпляра объекта. На объект может ссылаться локальная переменная, созданная на стеке, таблиц описателей в ситуациях внешнего взаимодействия и P/Invoke сценариях, из регстров (это может быть this-указать и аргусенты метода в течении выполнения метода) или из очереди завершителя (finalizer) для объектов имеющих завершающие методы (finalizer methods). OBJECTREF не указывает на начало экземпляра объекта, а указывает со смещением в 4 байта (DWORD) от начала. DWORD называется заголовком объекта и содержит индекс (номер синхронизирующего блока synblk, начинающийся с единицы) в таблице SyncTableEntry. Так как распределение происходит через индекс, CLR может переместить таблицу в памяти когда необходимо увеличение размера. The SyncTableEntry обслуживает мягкие ссылки обратно к объекту, так что владение блоком синхронизации может быть прослежено CLR. Мягкие ссылки позволяют сборщику мусора выполнять очистку, когда уже не существуют другие жёсткие ссылки. SyncTableEntry также хранит указатель на SyncBlock содержащий полезную информацию, но менее необходимую для всех экземпляров объекта. Эта информация включает блокировки объекта, его хеш-код, любые данные преобразования и индекс домена (AppDomainIndex). Для большинства экземпляров объектов, не будет существовать пространства выделенного для блока синхронизации (SyncBlock) и номер syncblock будет равен нулю. Это изменится когда выполняемый поток наткнется на выражение lock (obj) или obj.GetHashCode, как показано ниже:
SmallClass obj = new SmallClass() // Do some work here
lock(obj) { /* Do some synchronized work here */ }
obj.GetHashCode();
Рисунок 6 Представление экземпляра объекта
В этом коде, smallObj будет использовать ноль (нет syncblk) в качестве его номера в таблице блоков синхронизации (Syncblk Entry Table). Инструкция lock заставляет CLR создать syncblock запись и записать в заголовок соответствующий номер. Поскольку ключевое слово lock в С# развертывается в блок try-catch с использованием класса Monitor, объект Monitor создаётся в SyncBlock для синхронизации. Вызов метода GetHashCode () заполняет поле Hashcode хэш-кодом объекта в SyncBlock.
SyncBlock содержит другие поля используемые в взаимодействии с COM и маршалинге делегатов в неуправляемый код, но не относящиеся к типичному использованию объектов.
Хендлер типа (TypeHandle) следуют за номером syncblk в экземпляре объекта. В порядке поддержания непрерывности рассуждения, я буду обсуждать хендлер типа после разъяснения экземпляров переменных. Переменный список полей экземпляра следует за хендлером типа. По умолчанию, поля экземпляра размещаются таким образом, что бы использование памяти было эффективным и пропуски при выравнивании были минимальны. Код на Рисунке 7 содержит простой класс SimpleClass имеющий набор переменных экземпляра содержащихся в нём, с различными размерами.
class SimpleClass
{
private byte b1 = 1; // 1 byte
private byte b2 = 2; // 1 byte
private byte b3 = 3; // 1 byte
private byte b4 = 4; // 1 byte
private char c1 = 'A'; // 2 bytes
private char c2 = 'B'; // 2 bytes
private short s1 = 11; // 2 bytes
private short s2 = 12; // 2 bytes
private int i1 = 21; // 4 bytes
private long l1 = 31; // 8 bytes
private string str = "MyString"; // 4 bytes (only OBJECTREF)
//Total instance variable size = 28 bytes
static void Main()
{
SimpleClass simpleObj = new SimpleClass();
return;
}
}
Рисунок 8 содержит пример экземпляра объекта SimpleClass отображаемый в окне памяти отладчика Visual Studio. Мы установили точку останова на операторе return рисунок 7 и использовали адрес simpleObj содержащийся в регистре ECX чтобы отобразить экземпляр объекта в окне просмотра памяти. Первый 4-х байтовый блок это номер syncblk. Мы не используем экземпляр в любом коде требующем синхронизации (и не обращаемся к методу HashCode), поэтому это поле установлено в 0. Ссылка на объект сохранена в переменной стека, указывает на 4 байта, расположенные со смещением 4. Байтовые переменные b1, b2, b3 и b4 располагаются бок о бок друг с другом. The Byte variables b1, b2, b3, and b4 are all packed side by side. Обе переменные типа short s1 и s2 размещены. Строковая переменная str это 4-х байтовый ODJECTREF указывающий на актуальный экземпляр строки расположенный в куче сборщика мусора. Строка (String) специальный тип, все экземпляры содержащие одинаковый текст будут указывать на один и тот же экземпляр в глобальной таблице строк — это выполняется в процессе загрузки сборки. Этот процесс называется интернированием строк и спроектирован для оптимизации использования памяти. Как мы заметили ранее в .NET Framework 1.1 сборка не может отключить процесс интернирования, возможно в будущих версиях среды исполнения CLR будет предоставлена такая возможность.
Рисунок 8 Отладочное окно отображающее экземпляр объекта в памяти
Таким образом лексическая последовательность членов переменных в исходном коде не поддерживается в памяти по умолчанию. В сценариях внешнего взаимодействия, где лексическая последовательность должна быть перенесена в память, атрибут StructLayoutAttribute может быть использован, который принимает значение перечисления LayoutKind в качестве аргумента. LayoutKind.Sequential будет обеспечивать лексическую последовательность для маршализированных данных. В .NET Framework это не повлияет на управляемый макет (в версии .NET Framework 2.0 применение атрибута будет иметь эффект). В сценариях внешних взаимодействий где вам на самом деле необходимо иметь дополнительное смещение и явный контроль над последовательностью полей, LayoutKind.Explicit может быть использован совместно с атрибутом FieldOffset на уровне поля. Взглянув на непосредственное содержимое памяти, давайте воспользуемся отладчиком SOS чтобы посмотреть нва содержимое экземпляра объекта. Одна полезная команда это DumpHeap, которая позволяет выводить всё содержимое кучи и все экземпляры определённого типа. Вместо использования регистров, DumpHeap может показать адрес только что созданного нами объекта:
!DumpHeap -type SimpleClass
Loaded Son of Strike data table version 5 from
"C:/WINDOWS/Microsoft.NET/Framework/v1.1.4322/mscorwks.dll"
Address MT Size
00a8197c 00955124 36
Last good object: 00a819a0
total 1 objects
Statistics:
MT Count TotalSize Class Name
955124 1 36 SimpleClass
Общий размер объекта 36 байт. Не имеет значения, на сколько велика строка, экземпляры SimpleClass содержат только DWORD OBJECTREF. Переменные экземпляра SimpleClass занимают только 28 байт. Оставшиеся 8 байтов включают хендлер типа TypeHandle (4 байта) и номер блока синхронизации syncblk (4 байта). Получив адрес экземпляра simpleObj, давайте снимем дамп содержимого этого экземпляра использую команду DumpObj, как показано здесь:
!DumpObj 0x00a8197c
Name: SimpleClass
MethodTable 0x00955124
EEClass 0x02ca33b0
Size 36(0x24) bytes
FieldDesc*: 00955064
MT Field Offset Type Attr Value Name
00955124 400000a 4 System.Int64 instance 31 l1
00955124 400000b c CLASS instance 00a819a0 str
<< some fields omitted from the display for brevity >>
00955124 4000003 1e System.Byte instance 3 b3
00955124 4000004 1f System.Byte instance 4 b4
Как отмечено, макетом размещения по умолчанию, сгенерированным для классов компилятором C# является LayoutType.Auto (для структур используется LayoutType.Sequential); таким образом загрузчик классов переупорядочивает поля экземпляра для минимизации смещений. Мы можем использовать ObjSize для получения графа включающего пространство, занятое экземпляром, str. Здесь полученный вывод:
! ObjSize 0×00a8197c
sizeof (00a8197c) = 72 (0×48) bytes (SimpleClass)
Son of Strike
SOS отладочное расширение используемое для отображения содержимого структур данных CLR в этой статье. Это часть пакета установки .NET Framework и расположено по пути %windir%\Microsoft.NET\Framework\v1.1.4322. До загрузки SOS в процесс, включите управляемую отладку в свойствах проекта в Visual Studio .NET. Добавьте директорию в которой расположен SOS.dll в переменную окружения PATH. Для загрузки SOS при остановке в точке останова, откройте Debug | Windows | Immediate. В окне immediate выполните .load sos.dll. Используйте! help для получения списка команд отладчика. Для более подробной информации о SOS смотрите документацию msdn Bugslayer column.
Если вы отнимите размер экземпляра SimpleClass (36 байтов) от всего размера графа объектов (72 байта), вы получите размер str, который составляет 36 байт. Давайте проверим это сняв дамп экземпляра str. Ниже результат вывода команды:
!DumpObj 0x00a819a0
Name: System.String
MethodTable 0x009742d8
EEClass 0x02c4c6c4
Size 36(0x24) bytes
Если вы добавите размер экземпляра строки str (36 байт) к размеру экземпляра SimpleClass (36 байт), вы получите общий размер 72 байта, что соответствует выводу команды ObjSize. Заметьте, что ObjSize не будет включать память занятую инфраструктурой syncblk. Также, в .NET Framework 1.1, CLR не известно о памяти занятой любыми неуправляемыми ресурсами, такими как GDI объекты, COM объекты, файловые хендлеры, и так далее; поэтому они не будут отражены этой командой.
TypeHandle, указатель на таблицу методов MethodTable, расположен прямо после номера syncblk. До создания экземпляра объекта, CLR просматривает загруженные типы и загружает информацию о типе если тип не обнаружен, получает адрес таблицы методов, создаёт экземпляр объекта и заносит значение в TypeHandle экземпляра объекта. Код скомпилированный JIT компилятором использует хендлер типа TypeHandle для нахождения таблицы методов MethodTable для распределения методов. The JIT compiler-generated code uses TypeHandle to locate the MethodTable for method dispatching. CLR использует хендлер типа TypeHandle, когда необходимо найти загруженный тип через таблицу методов MethodTable.
Таблица методов MethodTable
Каждый класс и интерфейс, когда загружен в домен приложения, будет представлен в памяти структурой данных MethodTable. Это является результатом действий по загрузке классов до создания самого первого экземпляра объекта. В то время как экземпляр объекта ObjectInstance хранит состояние, MethodTable хранит информацию о поведении. MethodTable связывает экземпляр объекта с отображёнными в памяти структурами метаданных сгенерированными компилятором языка с помощью EEClass. Информация в таблице методов MethodTable и структуры данных, прикреплённые к ней могут быть доступны из управляемого кода через System.Type Указатель на таблицу методов может быть также получен даже в управляемом коде через свойство Type.RuntimeTypeHandle. Хендлер типа TypeHandle содержащийся в ObjectInstance, указывает на смещение от начала таблицы методов. Это смещение составляет 12 байт по умолчанию и содержит информацию для сборщика мусора, которая здесь обсуждаться не будет.
Рисунок 9 показывает типичное представление таблицы методов. Мы покажем некоторые важные поля хенлера типа, но для более полного списка используйте рисунок. Давайте начнём с Base Instance Size, так как он имеет прямую корреляцию с профилем памяти времени выполнения.
Рисунок 9 Представление таблицы методов
Базовый размер экземпляраBase Instance Size
Базовый размер экземпляра это размер объекта, вычисляемый загрузчиком класса, основанный на декларациях полей в коде. Как рассмотрено ранее, текущая реализация сборщика мусора требует размер экземпляра объекта как минимум 12 байт. Если класс не имеет ни одного объявленного экземплярного поля, это будет нести избыточные 4 байта.
Остальные 8 байт будут заняты заголовком (Object Header) (который может содержать номер блока синхронизации syncblk) и хендлером типа (TypeHandle). Снова размер объекта может быть подвержен влиянию StructLayoutAttribute.
Посмотрим на снимок памяти (окно памяти в Visual Studio .NET 2003) таблицы методов для MyClass из рисунка 3 (MyClass с двумя интерфейсами) и сравним это с генерированным с помощью SOS выводом. На рисунке 9, размер объекта расположен по 4-х байтовому смещению и имеет значение 12 (0×0000000C) байт. Следующее является выводом DumpHeap из SOS:
!DumpHeap -type MyClass
Address MT Size
00a819ac 009552a0 12
total 1 objects
Statistics:
MT Count TotalSize Class Name
9552a0 1 12 MyClass
Таблица слотов методов
Встроенная в таблице методов таблица слотов указывает на соответствующие описатели методов (MethodDesc), предоставляющие поведение типа. Таблица слотов методов создаётся на базе линейного списка объявлений методов располагающихся в следующем порядке: наследованные виртуальные методы, объявленные виртуальные методы, экземплярные методы, статические методы. Загрузчик классов идёт через метаданные текущего класса, родительского класса и интерфейсы и создаёт таблицу методов. В процессе формирования заменяются переопределённые виртуальные методы, заменяются скрываемые методы родительского класса, создаются новые слоты и дублируются слоты по необходимости. Дублирование слотов необходимо для создания иллюзии что каждый интерфейс имеет свою собственную мини vtable. Однако, слоты дубликаты указывают на ту же физическую реализацию.MyClass имеет три экземплярных метода, конструктор класса (.cctor) и конструктор объекта (.ctor). Конструктор объекта автоматически генерируется компилятором C# для всех объектов не имеющих конструкторов определённых явно. Конструктор класса генерируется компилятором когда мы имеем статические переменные определённые и инициализированные. Рисунок 10 показывает представление таблицы методов для MyClass. Представление показывает 10 методов потому что имеет место дублирование слота Method2 для IVMap, который будет рассмотрен следующим. Рисунок 11 показывает редактируемый SOS дамп таблицы методов класса MyClass.
Рисунок 10 Представление таблицы методов MyClass
!DumpMT -MD 0x9552a0
Entry MethodDesc Return Type Name
0097203b 00972040 String System.Object.ToString()
009720fb 00972100 Boolean System.Object.Equals(Object)
00972113 00972118 I4 System.Object.GetHashCode()
0097207b 00972080 Void System.Object.Finalize()
00955253 00955258 Void MyClass.Method1()
00955263 00955268 Void MyClass.Method2()
00955263 00955268 Void MyClass.Method2()
00955273 00955278 Void MyClass.Method3()
00955283 00955288 Void MyClass..cctor()
00955293 00955298 Void MyClass..ctor()
Первые 4 метода любого типа будут всегда ToString, Equals, GetHashCode и Finalize. Эти методы виртуальные наследованные от System.Object. Слот Method2 имеет дубликат, но оба указывают на один и тот же дескриптор метода. Явно кодированный .cctor и .ctor будут сгруппированы со статическими и экземплярными методами соответственно.
Описатель метода
Описатель метода (MethodDesc) это инкапсуляция реализации метода как его понимает CLR. Существует много типов описателей методов, что поддерживают вызовы к различным реализациям внешних взаимодействий, в добавок к управляемым реализациям. В этой статье мы будем рассматривать, только управляемый MethodDesc в контексте код, а показанного на рисунке 3. MethodDesc сгенерирован как часть процесса загрузки класса и первоначально указывает на промежуточный язык (IL). Каждый описатель метода MethodDesc заполнен содержимым PreJitStub, который отвечает за включение JIT компиляции. Рисунок 12 показывает типичное представление. Запись слота таблицы методов на самом деле указывает на заглушку вместо настоящей структуры данных MethodDesc. Эта запись располагается по отрицательному смещению из 5-и байт от настоящего MethodDesc и является частью 8-и байтового заполнения, наследуемого каждым методом. Эти 5 байт содержат инструкции для вызова подпрограммы PreJitStub. Это 5-и байтовое смещение может быть видно из вывода DumpMT (для MyClass на рисунке 11) of SOS, поскольку MethodDesc всегда 5 байт после расположения указанного в записи таблицы слотов методов. До первого вызова выполняется вызов подпрограммы JIT компиляции. После выполнения компиляции 5 байт содержащие инструкцию вызова будут перезаписаны командой безусловного перехода на JIT скомпилированный код в архитектуре x86.
Рисунок 12 Описатель метода
Дизассемблирование кода на который указывает запись в таблице слотов методов на рисунке 12 будет показывать вызов на PreJitStub. Здесь сокращённый вывод дизассемблирования до JIT компиляции для метода Method2:
!u 0x00955263
Unmanaged code
00955263 call 003C3538 ;call to the jitted Method2()
00955268 add eax,68040000h ;ignore this and the rest
;as !u thinks it as code
Теперь давайте запустим метод и дизассемблируем тот же адрес:
!u 0x00955263
Unmanaged code
00955263 jmp 02C633E8 ;call to the jitted Method2()
00955268 add eax,0E8040000h ;ignore this and the rest
;as !u thinks it as code
Только первые 5 байт по этому адресу являются кодом; остальные содержат данные метода Method2 описателя методов. Команда »! u» не в курсе этого и генерирует бессмысленный код, то есть вы можете игнорировать всё после 5-и первых байт.
CodeOrIL до JIT компиляции содержит относительный виртуальный адрес (RVA) реализации метода в промежуточном языке (IL). Это поле устанавливается для индикации что это есть промежуточный код. CLR обновляет это это поле адресом JIT –компилированного кода после компиляции по требованию. Давайте выберем метод из тех что выведены и снимем дамп MethodDesc используя команду DumpMT до и после JIT компиляции:
!DumpMD 0x00955268
Method Name : [DEFAULT] [hasThis] Void MyClass.Method2()
MethodTable 9552a0
Module: 164008
mdToken: 06000006
Flags : 400
IL RVA : 00002068
После компиляции, MethodDesc выглядит следующим образом:
!DumpMD 0x00955268
Method Name : [DEFAULT] [hasThis] Void MyClass.Method2()
MethodTable 9552a0
Module: 164008
mdToken: 06000006
Flags : 400
Method VA : 02c633e8
Поле флагов в описателе метода закодировано для хранения информации о типе метода, таким как статичный, экземплярный, интерфейсный метод или COM реализация.
Давайте посмотрим на другой сложный аспект таблицы методов: реализация интерфейсов. Она сделана так, чтобы смотреть просто на управляемую среду понимая все сложности в процесс представления. Далее, мы рассмотрим как интерфейсы размещаются и как распределение интерфейсных методов реально работает.
IVMap и Карта интерфейсов
По смещению 12 в таблице методов находится важный указатель, IVMap. Как показано на рисунке 9, IVMap указывает на таблицу сопоставлений уровня домена прил