.NET offers a managed memory system. This is just a short way of describing a system which handles and manages the RAM memory in its own specific way.
.NET uses a stack and a heap to store all the information required for a program to run. The two components work together to enable a program to run.
Stack
The stack is a memory container in which .NET stores all the value types. Each time a value type is used, it will be added (aka pushed, if we use the stack terminology) to the stack until it gets out of scope (a method execution is finished) and gets deleted (aka popped). A value type can be standalone (an int) or a link (aka pointer) to a reference type.
The stack memory is dynamic, meaning that each time a value type is out of scope, the memory allocated to that type is freed and the stack cleared.
Each time a method is called, all the variables will be present in the stack until they are out of scope. If reference types are used in the method, there will be pointers to the reference types present in the heap) .
The stack also contains all the calls which are made to different methods, also called the call stack. The list of all the calls which are made is also known as the stack trace.
Being a stack, all its members are added/removed following the LIFO order (Last In First Out).
The stack is faster than the heap but more limited from the memory point of view (a lower number of types in stack than in heap).
Heap
The heap is also a memory container in which .NET stores all the reference types. Value types usually link to a reference type, in other words they reference an instance of an object, thus the name referenced types.
The heap is slower than the stack, but much larger in size, so it can hold more instances.
A reference of an instance can be lost at some point by design (out of scope, setting it to null) and then referenced type remains isolated in the heap waiting for the Garbage Collector to destroy and release the allocated memory. We’ll get into the details of Garbage Collector in a future article.
Code examples
Let’s take some practical examples to better understand how the stack and heap work together.
Value Types
Consider the following code:
int i = 5;
We used a type value and assigned its value to 5. The stack will contain the i variable with the value of 5. The heap will be empty since we only used a value type. So we will have something like this:
Reference Types
Consider the following code:
class Person { public int Age {get;set;} } class Test { static void Main() { Person ceo; ceo = new Person(); ceo.Age = 27; } }
We have a variable, ceo which is of type Person, so this will go in the stack. At this point in time we have no referenced types instantiated so the heap is now empty.
Then we instantiate ceo by using the new keyword. At this point in time, an object of type Person is created in the heap because it’s a referenced type. The memory allocated for the instance of Person is that of one int which is set to the default value of int, 0. There is no pointer from the variable to the instantiated object yet.
After we instantiated Person type using the new keyword, we assign its value to the ceo variable by using the equal operator. It is now the point when the variable is linked to the actual instance by using a pointer.
We then set the value of Age to 27, so the Age will get updated in the heap.
So we will have something like this:
The variables in the stack are linked to the referenced types through a pointer, which is just a link to a dedicated memory called memory address. This memory address is represented by numbers. In our case, the referenced type was created on the 1234 memory address. Our variable, will only know that it’s represented by whatever is in that memory address.
Example 1
Let’s take another, more complex example:
class Person { public int Age {get;set;} } class Test { static void Main() { int age; //step 1 age = GetAge(); //step 7 //step 8 }//step 9 static int GetAge() { Person ceo; //step 2 ceo = new Person(); //step 3 and step 4 ceo.Age = 27; //step 5 return ceo.Age; //step 6 } }
Step 1: value type age is pushed to stack with the default value of 0;
Step 2: we call a method. Stack keeps track of all methods called and allocates space required for all operations in method until it goes out of scope. Therefore we will have ceo variable added to stack with the default value of null;
Step 3: we instantiate the Person type, a new referenced type is added to the heap. The object has just an int so memory will be allocated for one int only in the heap. The int takes the default value of 0;
Step 4: Since we assign the new reference to the variable, the pointer between the two is created and therefore are linked (123 is the memory address of the pointer);
Step 5: age is set to 27, the int in the heap will be updated from 0 to 27;
Step 6: the age is returned by the method;
Step 7: the returned value is assigned to the value type;
Step 8: GetAge method is out of scope so all required memory will be cleared from the stack. At this point, we no longer have a pointer to the instance of type Person in heap. The instance remains in heap until GarbageCollector will remove it and release the used memory;
Step 9: Main method gets out of scope, thus all the used memory allocated for it is released from the stack.
So we will have something like this:
Note that GarbageCollector will remove all unused objects from the heap at some point, but the exact time is arbitrary, unless GarbageCollector is implicitly called.
Example 2
Let’s take a final and interesting example:
class Person { public int Age {get;set;} } class Test { static void Main() { Person person; //step 1 person = new Person(); //step 2 and 3 person.Age = 27; //step 4 Person anotherPerson; //step 5 anotherPerson = new Person(); //step 6 and 7 anotherPerson = person; //step 8 anotherPerson.Age = 29; //step 9 Console.WriteLine(person.Age); // prints 29 } }
The above example prints 29. But why is that? You’d expect to print 27, right? Let’s take a look about what happens at the memory level.
Step 1: person variable is pushed to the stack;
Step 2: new Person object is instantiated in the heap with the new keyword. The Age property is initialized with the default value of 0. The memory address is 1234;
Step 3: variable person is assigned the reference of the newly instantiated object Person. The link is created between the two using a pointer with the memory address of 1234;
Step 4: the Age property is set in the heap to the value of 27.
Step 5: we create another variable called anotherPerson which is pushed to the stack;
Step 6: another object of type Person is instantiated in the heap since we used the new keyword. The reference has a memory address of 2345;
Step 7: variable anotherPerson is assigned the reference of the newly instantiated object Person from the heap. The link is created between the two using a pointer with the memory address of 2345;
Step 8: this is the interesting part. anotherPerson variable is assigned another memory reference, the one of person. So the initial pointer with memory address of 2345 is replaced with the other pointer from the person variable which points to the memory address of 1234. So both variables point to the same instance in the heap. The instantiated object with memory address 2345 is left orphan and is ready to be handled by the GarbageCollector;
Step 9: We change the value of the Age property in the heap from 27 to 29. Keep in mind that both variables now reference the same object in the heap. In other words, person and anotherPerson are now one and the same thing. This is why when we print the Age value of person we will get the value of 29.
This is what graphically happens in the heap:
For more information check out Jamie King’s videos, they are super easy to understand and cover more scenarios.