Introduzione
Nell’articolo precedente, abbiamo iniziato ad approfondire cosa accade nel momento in cui il CLR alloca un oggetto sulla memoria Heap e come vengono distribuite le informazioni contenute nell’oggetto stesso, sia in termini di dati propri dell’oggetto che di dati aggiunti dal CLR.
In questo articolo, vedremo nel dettaglio come avviene l’invocazione dei metodi sugli oggetti con e senza polimorfismo.
La Method Table
Ciascun Type Object allocato sull’Heap ed associato ad uno specifico tipo, contiene al suo interno la Method Table, ossia la tabella dei metodi esposti dal tipo stesso che viene utilizzata dal CLR per l’invocazione di questi ultimi sugli oggetti.
Utilizzando il debugger avanzato SOS di Visual Studio, è possibile visualizzare la Method Table di un tipo. Eseguiamo in primo luogo il dump dell’oggetto bClass in modo da poter ricavare l’indirizzo della Method Table all’interno del corrispondete Type Object, BaseClass Type Obj.
!DumpObj 00c2c3b0
PDB symbol for clr.dll not loaded
Name: ConsoleApplication1.BaseClass
MethodTable: 009b391c
EEClass: 009b14d8
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 3 staticInt
Dall’esecuzione del comando di DumpObj, si evince che la Method Table si trova all’indirizzo 0x009b391c. Su tale indirizzo, possiamo eseguire il comando DumpMT :
!DumpMT -MD 009b391c
EEClass: 009b14d8
Module: 009b2e9c
Name: ConsoleApplication1.BaseClass
mdToken: 4a0ec1b602000002
File: C:\Documents and Settings\Developer\My Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
BaseSize: 0x10
ComponentSize: 0x0
Slots in VTable: 9
Number of IFaces in IFaceMap: 0
————————————–
MethodDesc Table
Entry MethodDesc JIT Name
79aaa7e0 79884934 PreJIT System.Object.ToString()
79aae2e0 7988493c PreJIT System.Object.Equals(System.Object)
79aae1f0 7988495c PreJIT System.Object.GetHashCode()
79b31600 79884970 PreJIT System.Object.Finalize()
009bc048 009b38e8 NONE ConsoleApplication1.BaseClass.VirtualMethod()
009bc038 009b38cc JIT ConsoleApplication1.BaseClass..ctor()
009bc058 009b3904 JIT ConsoleApplication1.BaseClass..cctor()
009bc040 009b38d8 NONE ConsoleApplication1.BaseClass.InstanceMethod()
009bc050 009b38f4 NONE ConsoleApplication1.BaseClass.StaticMethod()
In primo luogo, osserviamo che il numero di slot nella tabella sono 9 (quanti sono i metodi invocabili sul tipo BaseClass). I primi quattro metodi sono ereditati dalla classe Object, che come sappiamo è la classe base per un qualsiasi oggetto nel .Net Framework. Si evince che essi hanno l’entry point (colonna Entry) ad un indirizzo di memoria completamente diverso dagli altri, in quanto l’oggetto Object Type Object, viene istanziato dal CLR all’avvio in una propria zona della memoria Heap ed il codice viene subito compilato dal JIT compiler (da notare la voce PreJIT).
Subito dopo, ci sono i tre metodi della classe BaseClass, che non essendo stati ancora invocati, non sono stati compilati dal JIT compiler (valore NONE). Viceversa, i due costruttori.ctor e .cctor (di istanza e di tipo, rispettivamente) sono stati eseguiti al momento della creazione dell’oggetto bClass e quindi compilati dal JIT compiler.
Invocazione di un metodo di istanza non virtuale…
A questo punto, eseguiamo la seguente riga di codice :
bClass.InstanceMethod();
Il metodo in questione è un metodo di istanza non virtuale, per cui il JIT compiler deve semplicemente individuare il tipo con cui è stato dichiarato l’oggetto bClass, BaseClass, ed accedere alla Method Table del BaseClass Type Object corrispondente. Individuato il metodo, lo compila e lo esegue.
Figura 1
Se rieseguiamo il dump della tabella dei metodi :
009bc048 009b38e8 NONE ConsoleApplication1.BaseClass.VirtualMethod()
009bc038 009b38cc JIT ConsoleApplication1.BaseClass..ctor()
009bc058 009b3904 JIT ConsoleApplication1.BaseClass..cctor()
009bc040 009b38d8 JIT ConsoleApplication1.BaseClass.InstanceMethod()
009bc050 009b38f4 NONE ConsoleApplication1.BaseClass.StaticMethod()
Osserviamo che il metodo InstanceMethod() risulta essere stato compilato dal JIT compiler.
… di un metodo virtuale …
Passiamo adesso ad invocare il metodo virtuale :
bClass.VirtualMethod();
In questo caso, avendo a che fare con un metodo virtuale, non basta al JIT compiler sapere con quale tipo è stata dichiarata la variabile bClass, perchè utilizzando l’ereditarietà, potremmo anche far puntare la variabile bClass (dichiarata come BaseClass) ad una istanza della classe derivata DerivedClass. In questo caso, il tipo utilizzato per la dichiarazione della variabile non coinciderebbe con il tipo dell’effettivo oggetto puntato (come vedremo successivamente) e la tecnica utilizzata per l’invocazione di un metodo di istanza non virtuale non funzionerebbe.
In una situazione del genere, il JIT compiler deve accedere all’oggetto effettivamente puntato dalla variabile bClass, cioè nel nostro caso il BaseClass Obj, e deve analizzare il campo Type Object Pointer per poter ricavare l’effettivo Type Object. In questo caso, esso è ancora ilBaseClass Type Obj. A questo punto, il JIT accede alla Method Table, individua il metodo, lo compila e lo esegue.
Figura 2
… infine di un metodo statico
Infine, vediamo cosa accade per l’invocazione del metodo statico :
BaseClass.StaticMethod();
Questa situazione, è molto simile all’invocazione del metodo di istanza non virtuale, con la differenze che il JIT compiler conosce immediatamente la classe a cui appartiene il metodo (considerato il modo in cui viene eseguita l’invocazione) e quindi conosce subito il Type Object (in questo caso il BaseClass Type Obj) nel quale cercare il metodo nella tabella.
Allocazione di un oggetto della classe derivata …
Passiamo ora a vedere cosa accade utilizzando un riferimento del tipo BaseClass, per puntare ad un oggetto della classe derivata DerivedClass e quindi invocare su di essa il metodo virtuale sfruttando a tutti gli effetti il concetto di polimorfismo.
In primo luogo, un’istanza della classe DerivedClass deve essere allocata :
bClass = new DerivedClass();
Con l’istruzione precedente, il CLR deve allocare in memoria Heap un’istanza della classeDerivedClass e fare in modo che il suo Type Object Pointer punti, ovviamente, alDerivedClass Type Obj. L’indirizzo assegnato al nuovo oggetto allocato verrà restituito nella variabile bClass.
Figura 3
Osservando la Figura 3, si evince che il nuovo oggetto DerivedClass Obj è stato allocato sull’Heap e che la variabile bClass punta ad esso. Non c’è più alcun riferimento all’oggettoBaseClass Obj che quindi sarà sottoposto alla pulizia da parte del Garbage Collector.
Analizziamo nel dettaglio l’area di memoria in cui è stato allocato il nuovo oggetto :
Figura 4
In riferimento alla Figura 4, di seguito c’è il dettaglio dei valori contenuti in memoria :
- Offset –4 (val. 00000000) : è il Sync Block Index;
- Offset 0 (val. 009b39a4) : è il Type Object Pointer;
- Offset +4 (val. 00c2c3c0) : campo instanceString, ereditato dalla classe baseBaseClass. Da osservare che l’indirizzo è ovviamente il medesimo;
- Offset +8 (val. 00000001) : campo instanceInt, ereditato dalla classe base BaseClass;
… ed invocazione di un metodo virtuale overridden
Per concludere, vediamo cosa accade durante l’invocazione del metodo virtuale di cui la classe derivata ne fa l’override :
bClass.VirtualMethod();
In questo caso, il JIT compiler parte dalla variabile bClass che punta all’oggetto DerivedClass Obj sulla memoria Heap. A partire da tale oggetto, analizza il suo Type Object Pointer che punterà al DerivedClass Type Obj. In questo modo, il JIT compiler arriva alla Method Tabledella DerivedClass, compila ed esegue il metodo virtuale giusto e non quello della classe base.
Figura 5
Con questa seconda parte, si conclude l’approfondimento relativo al CLR memory model mettendo in evidenza che nonostante le notevoli semplificazioni che il .Net Framework ci fornisce per sviluppare le nostre applicazione nel modo più rapido possibile, l’architettura sottostante del sistema non è assolutamente semplice.