As you know, in .NET memory is divided into two categories: the stack (Stack) and the managed heap (managed heap, then just a heap). On the stack is a link (ObjectRef) to the object (ObjectInstance), which, in turn, is located on the heap.
This article will focus on the location of the object in the heap.
It is assumed that the reader has knowledge of:
1. stack
2. managed heap
3. GC
4. Weak references
')
ObjectInstance
ObjectInstance is a data structure (object) located at the address pointed to by ObjectRef.
Instances of significant types can be stored:
1. in the thread stack
2. in GC Heap.
Instances of reference types, depending on their size, can be stored:
1. in the Garbage Collector Heap (objects up to 85,000 bytes in size)
2. in Large Object Heap (objects larger than 85000 bytes)
Figure 1. Sample object instance structureAs can be seen in Figure 1, ObjectInstance contains the following fields:
1. Object Header
2. Type Handle (consideration of this field is beyond the scope of the article)
3. Object instance fields
4. Links to string literals
Object header
Take another look at Figure 1. As you can see, the ObjectRef points to an address that is offset by 4 bytes from the beginning of ObjectInstance. This is done for optimization - the Object Header contains information that is not used as often as the other fields of an object instance.
This field contains the cell number in the Sync Block Entry Table. Number is counted from one. If the lock statement or the GetHashCode () method has never been used for ObjectInstance, then the 0 0 will be stored in the Object Header cell.
Since the Sync Block Entry Table and ObjectInstance are linked by index, the CLR can freely change the size of this table in memory.
The cell of the Sync Block Entry Table, the index of which is contained in the Object Header, in turn contains a weak reference to the same ObjectInstance - this is done so that the CLR can track the owner of this cell (for example, to synchronize threads).
Also, this cell contains a reference to the SyncBlock object in the SyncBlock List structure. SyncBlock contains useful but rarely used information:
1. Information about locking (thunking)
2. AppDomain Index
3. Data on object blocking
4. Hash code
Consider an example. Given code:
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
class Sample { TestClass sync = new TestClass (); void Main() { lock (sync) { //, } sync.GetHashCode(); } } * This source code was highlighted with Source Code Highlighter .
In line 3, the sync object contains the value 0 in the Object Header field. Next, in line 7, the field value changes and becomes nonzero. This is due to the fact that the CLR creates a SyncBlock object in the SyncBlock List and places the index of the record in the Object Header. On line 11, the hash code value is placed in the SyncBlock object.
Consideration of the remaining fields of the SyncBlock object is beyond the scope of this article.
Object instance fields and string literals
For Type Handle is a list of fields of the instance, the length of which is different. By default, the fields of the instance are arranged so that the memory is used efficiently and the gaps between the fields are minimal.
Listing 1 shows the SimpleClass class, which declares several instance variables of different sizes.
Listing 1 |
- 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 (this is only OBJECTREF)
- // The total size of the instance fields is 28 bytes
- static void Main ()
- {
- SimpleClass simpleObject = new SimpleClass ();
- return ;
- }
- }
* This source code was highlighted with Source Code Highlighter .
|
Figure 2. Memory window for SimpleClass Object InstanceIn fig. Figure 2 shows what an instance of the SimpleClass object looks like in the memory window.
Before opening the Debugger Memory Window (Debug-> Windows-> Memory-> Memory1), the breakpoint was set on the return statement. Then, at the simpleObject address contained in the ECX register, the address of the object instance for which you need to look at the memory dump was determined.
1. The first 4-byte block is the syncblk number. Since we did not write the synchronization code for this object (and did not access its hash code), this number is 0.
2. A reference to an object stored in the variable simpleObject placed on the stack indicates 4 bytes, the beginning of which is at offset 4.
3. Next comes the eight-byte value of the variable l1 in memory.
4. The variable str of type String is represented by a 4-byte ObjectRef reference pointing to an instance of a string hosted in the GC Heap.
5. A variable i1 of type int is placed next, occupying 8 bytes.
6. Two variables of type char - c1 and c2 - are arranged side by side.
7. Two variables of the type short - s1 and s2 - are also located side by side.
8. Variables b1, b2, b3 and b4 of the type Byte are packed in a DWORD and arranged side by side.
By default, object members are not necessarily placed in memory in the order in which they are declared in the source code.
In order to keep their lexical sequence when placing fields in memory, the StructLayoutAttribute attribute must be applied. The attribute takes as input a LayoutKind enumeration that has the following values:
1. Sequential - The members of the object are arranged sequentially, in the order of their appearance when exported to unmanaged memory.
2. Explicit - The exact position of each member of an object in unmanaged memory is explicitly controlled. Each member must use the FieldOffsetAttribute attribute to indicate the position of this field within the type.
3. Auto - The CLR automatically selects the appropriate location for object members in unmanaged memory. Access to objects defined by this enumeration member cannot be granted outside of managed code. An attempt to perform such an operation will cause an exception.
Having examined the unstructured contents of the memory, we examine the object instance using the Son of Strike (SOS) utility.
Moving the project to unmanaged debug mode (Properties-> Debug-> Enable unmanaged code debugging), and opening the Immediate Window (ctrl + alt + I), we will execute the following commands:
- .load sos
- ! DumpHeap -type SimpleClass
* This source code was highlighted with Source Code Highlighter .
As a result of the output, we obtain the contents of all the heaps and all instances of the specified type:
Figure 3. The result of the "! DumpHeap -type SimpleClass" commandAs can be seen from Figure 3, the object size is 36 bytes. These 36 bytes are:
1. The SimpleClass instance fields are 28 bytes (the length of the string does not matter, since the SimpleClass instance contains only a reference to the literal table, 4 bytes in size.).
2. The remaining 8 bytes are contained in Object Header (4 bytes) and Type Handle (4 bytes).
We learned the address of the instance of simpleObject, now let's dump the contents of this instance with the command "! DumpObj 0x0214ba30":
Figure 4. The result of the "! DumpObj 0x0214ba30" commandThe C # compiler uses LayoutType.Auto for class members by default; therefore, the class loader can arrange fields as it sees fit to minimize gaps between fields.
Materials:
MSDN