CLR : Inside memory model (Parte 1)

Introduzione

Quotidianamante, sviluppando software con il .Net Framework ed in particolare con un linguaggio come il C#, ci preoccupiamo sempre molto poco di come si comporta il CLR durante l’esecuzione ed in particolare modo dell’allocazione e deallocazione della memoria, dell’esecuzione dei metodi sugli oggetti e quant’altro.

Ovviamente, siamo concentrati su ciò che riguarda l’implementazione della Business Logic nel nostro dominio applicativo, considerando che tutto il resto verrà eseguito per noi dal CLR.

Può essere molto utile, però, approfondire alcuni aspetti interni del .Net Framewok per ottenere un certo beneficio in termini di performance delle applicazioni sviluppate.

In particolare, con questo articolo, intendo descrivere alcuni meccanismi che sottintendono all’allocazione della memoria ed all’invocazione dei metodi sugli oggetti da parte del CLR.

Un pò di codice …

Supponiamo di avere la dichiarazione delle due seguenti classi :

internal class BaseClass
{
    private string instanceString = "InstanceString";
    private int instanceInt = 1;
    private static int staticInt = 2;

    public BaseClass() { }
    public int InstanceMethod() { return 0; }
    public virtual int VirtualMethod() { return 0; }
    public static int StaticMethod() { return 0; }
}

internal class DerivedClass : BaseClass
{
    public override int VirtualMethod() { return 1; }
}

e di avere il metodo Main() di un programma contenente le seguenti righe di codice :

BaseClass bClass = new BaseClass();
bClass.InstanceMethod();
bClass.VirtualMethod();
BaseClass.StaticMethod();
bClass = new DerivedClass();
bClass.VirtualMethod();
In corrispondenza di questa banalissima sequenza di istruzioni, cosa accade di preciso ? Come si comporta il CLR e come predispone il sistema per permetterci l’utilizzo dell’oggettobClass ?
I Type Object
In primo luogo, il CLR si assicura che gli assemblies nei quali sono definiti i tipi BaseClass eDerivedClass siano stati già caricati (altrimenti li carica). Successivamente, utilizza i metadati contenuti negli assemblies per allocare alcune strutture in memoria, i cosiddetti Type Object.
3364.mem_alloc_1_3939D87B
Figura 1
Prima di analizzare nel dettaglio la Figura 1, va fatta un’importante premessa. Ogni qual volta viene allocato un oggetto nella memoria Heap, esso non conterrà solo ed esclusivamente i dati che sono stati dichiarati al suo interno ma il CLR aggiungerà alcune informazioni necessarie per l’utilizzo dell’oggetto stesso. In particolare, vengono sempre aggiunti i seguenti 8 byte :
  • Sync Block Index (4 byte) : è un indice con il quale il CLR può puntare all’interno di una tabella con cui gestire alcune operazioni quali il lock su un oggetto (in contesti multithreading) oppure ricavare un hash code per l’oggetto stesso (metodoGetHashCode() ereditato dalla classe base Object);
  • Type Object Pointer (4 byte) : il puntatore all’oggetto rappresentativo del tipo (Type Object);

Facendo riferimento alla Figura 1, il CLR ha instanziato i due Type Object relativi ai tipiBaseClass e DerivedClass. Indipendentemente da quante istanze di queste due classi noi andremo ad allocare nel corso del programma, le istanze dei corrispondenti Type Objectrimarrano solo ed esclusivamente queste due.

Cosa contiente un Type Object ? Di seguito gli elementi fondamentali :

  • Sync Block Index e Type Object Pointer : essendo un oggetto a tutti gli effetti, anche un Type Object deve avere queste due informazioni;
  • Static Fields : i campi statici del tipo, considerando che essi saranno comuni a tutte le istanze del tipo stesso;
  • Method Table : la tabella dei metodi che viene utilizzata dal CLR nel momento in cui c’è un’invocazione di un metodo su un oggetto. In questo modo, si individua il metodo che dovrà essere eventualmente compilato dal JIT (in caso di prima invocazione) e poi eseguito;

Ma a cosa punta il Type Object Pointer di un Type Object ?

Type Object non sono nient’altro che istanze del tipo Type, molto usato con la Reflection. Esisterà, quindi, anche un’istanza del Type Object del tipo Type…ossia, scusate il gioco di parole, il Type Type Object ! Quest’ultimo sarà a sua volta un oggetto e quindi sarà dotato di un Type Object Pointer che punterà a se stesso.

Allocazione di un oggetto

A questo punto cosa accade quando andiamo ad allocare un’istanza della classe BaseClass(riga 1) ?

Viene predisposta l’area di memoria sull’Heap per accogliere l’oggetto e l’istruzione newritornerà l’indirizzo di tale area. Questo indirizzo sarà quindi assegnato alla variabile bClass, allocata sullo Stack.

1373.mem_alloc_2_79FA888F

Figura 2

Dalla Figura 2, si evince che l’istanza della classe BaseClass contiene :

  • Sync Block Index e Type Object Pointer : da notare come il Type Object Pointerpunti al BaseClass Type Object;
  • Instance Fields : i campi di istanza che quindi sono esclusivi per tale oggetto;
  • String Literals : le stringhe direttamente definite nella classe (literals);

Analisi avanzata in memoria

Per comprendere meglio la distribuzione di queste strutture in memoria, possiamo eseguire il codice e ispezionare la memoria.

8780.mem_alloc_3_265CA05B

Figura 3

Dalla Figura 3, possiamo in primo luogo osservare che la variabile bClass (allocata sullo Stack) ha come contenuto il valore 0x00c2c3B0 (indirizzo della memoria Heap); essa non punta esattamente all’inizio dell’oggetto (Sync Block Index) ma punta 4 byte dopo (Type Object Pointer). E’ da sottolineare che la rappresentazione dei byte è di tipo Little Endian (processore Intel).

Di seguito il dettaglio della memoria allocata per l’oggetto bClass, riportata per offeset rispetto al puntatore della variabile sullo Stack :

  • Offest –4 (val. 00000000) : è il Sync Block Index. Poichè non è stata eseguita alcuna istruzione di lock sull’oggetto, esso è zero.
  • Offeset 0 (val. 009b391c) : è il Type Object Pointer;
  • Offeset +4 (val 00c2c3c0) : campo instanceString che essendo di tipo String, quindi un reference type, è ovviamente un puntatore all’oggetto contenente la string literal “InstanceString”;
  • Offeset +8 (val. 00000001) : campo instanceInt il cui valore è 1;
  • Offeset + 12 (val. 00000080) : da qui inizia l’istanza dell’oggetto String relativa al campo instanceString; in questa posizione c’è il Sync Block Index che è diverso da 0, in quanto la string literal “InstanceString” potrebbe essere utilizzata in altre parti di codice ma allocata una sola volta;
  • Offeset +16 (val. 79b9f9ac) : è il Type Object Pointer dell’istanza String di cui sopra;
  • Offeset +20 (val. 0000000e) : è la dimensione della stringa “InstanceString” (14 caratteri);
  • Offeset +24 : da qui inizia la stringa “InstanceString” che occupa complessivamente 28 byte (14 caratteri), poichè la rappresentazione è Unicode (2 byte per carattere);

Questi valori possono essere confrontati con un’analisi eseguiti utilizzando la libreria di debug avanzato sos.dll attraverso la Immediate Window di Visual Studio.

!DumpObj 0x00c2c3b0
Name:        ConsoleApplication1.BaseClass
MethodTable: 009b391c
EEClass:     009b14d0
Size:        16(0x10) bytes
File:

C:\Documents and Settings\Developer\My Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
Fields:
MT    Field   Offset                 Type VT     Attr    Value Name
79b9f9ac  4000001        4        System.String  0 instance 00c2c3c0 instanceString
79ba2978  4000002        8         System.Int32  1 instance        1 instanceInt
79ba2978  4000003       24         System.Int32  1   static        2 staticInt

Eseguendo il comando DumpObj sull’indirizzo dell’oggetto, osserviamo che il valore del Type Object Pointer (009b391c) viene riportato come MethodTable, proprio perchè nel Type Object è contenuta la tabella dei metodi del tipo. Vengono inoltre elencati i campi dell’oggetto con i corrispondenti tipi, nomi e valori.

Inoltre, se eseguiamo il dump delle informazioni sul campo instanceString (indirizzo 00c2c3c0) otteniemo :

!DumpObj 00c2c3c0
Name:        System.String
MethodTable: 79b9f9ac
EEClass:     798d8bb0
Size:        42(0x2a) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      InstanceString
Fields:
MT    Field   Offset                 Type VT     Attr    Value Name
79ba2978  40000ed        4         System.Int32  1 instance       14 m_stringLength
79ba1dc8  40000ee        8          System.Char  1 instance       49 m_firstChar
79b9f9ac  40000ef        8        System.String  0   shared   static Empty

Da cui osserviamo proprio che all’offeset 4 c’è la dimensione della stringa e subito dopo inizia la stringa stessa con il primo carattere.

Nel prossimo articolo, vedremo cosa accade durante l’invocazione dei metodi su un oggetto ed in particolare per una classe derivata.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s