In modern programming languages such as C# or Java, we tend to take memory management for granted.
Gone are the days when we need to call malloc to request enough memory for our variables. Luckily a lot of that is done for us by the runtimes so we don’t usually need to allocate and deallocate memory.
Understanding how the underlying memory management works as well as where our variables are stored can still be really helpful when it comes to understanding the scope of our variables.
If you are programming in C or C++ then understanding heap memory and stack memory is going to be essential knowledge.
In this article, we are going to look at what stack and heap memory are, their key differences, and where your variables are stored when they are in memory.
Memory Layout
At a high level, the memory for your application is laid out like this:
This is slightly simplified as there are other areas of memory but this is enough to cover what we are looking at today.
At runtime, your compiled code is stored in memory as execution instructions in the bottom part of memory (machine code).
How do heap and stack grow?
The stack and heap both share the same address space. As shown in the diagram the heap grows upwards and more space is allocated to it as you need it, either manually by the programmer (C/C++) or automatically by the runtime.
The stack is located in the high address space. Items are added to the stack moving downwards going from high address space to low address space. However, the size of the stack is generally fixed when the application is compiled. This is why if you run a recursive function in an infinite loop you will get a stack overflow exception.
Even though the stack and heap take up space towards each other the operating system will make sure that they don’t consume the same address space.
What is the stack?
The stack has 2 main responsibilities when your program is running:
To keep track of the method that control should return to once execution has finished for the current method.
To hold the values (or pointers) of local variables used in the methods.
How does the call stack work?
Each time you call a method in your application it is added (pushed) to the call stack along with any local variables that are declared in the call stack.
Once the execution of that method has finished it is removed (popped) from the call stack and execution is returned to the previous method.
Stack Data Structure
To properly understand the call stack you need to understand the stack data structure.
The stack can be viewed similarly to a stack of books. You can add and remove books from the top of the stack but you can’t access any in the middle or the bottom.
The stack works on the Last In First Out (LIFO) principle. You can only ever read the data from the item on the top of the stack.
This is why when you call a new method you don’t have access to any of the variables that were used in the calling method. All of those variables are sitting in the previous block and are therefore inaccessible.
Stack vs Array
The stack isn’t like an array. In an array, you can access any item in the array using its index. With a stack, you only have access to the last item you added to it.
Stack vs Queue
A queue works on the First In First Out (FIFO) principle so it has some similarities to a stack. Like a queue, you can’t access items in the middle of the stack only those at the end of the queue.
A stack is basically a one-ended queue.
What is the heap?
The heap is a section of memory that allows dynamic allocation of memory and is not bound by the same rules as the stack.
This means that if you want to allocate memory to store a large amount of data then the heap is the best place to do it.
If you need access to data throughout your application then this data will also be stored on the heap.
Heap Management
If you are using a programming language such as Java or C# then memory on the heap will be allocated and deallocated for you. Both of these runtimes have “Garbage Collectors” (GC) which go through the process of cleaning up unused blocks of heap memory.
Allocating and deallocating memory on the heap has a performance impact as to is not as quick as adding and removing items from the stack.
Running profiling on your application will let you see how often the garbage collector is removing items from the heap. You can often get some good performance improvements if you can optimise how often memory is allocated.
Differences in Stack and Heap Memory
We have mentioned already some of the key differences between heap memory and stack memory.
The table below shows all the other differences in heap and stack memory:
Feature | Stack | Heap |
---|---|---|
Access Speed | Fast | Slow |
Memory Allocation | Handled automatically by runtime | Only automatically handled in high level languages |
Performance Cost | Less | More |
Size | Fixed Size | Dynamic Size |
Variable Access | Local variables only | Global variable access |
Data Structure | Linear data structure (stack) | Hierarchical Data Structure (array/tree) |
Main Issue | Small fixed amount of memory (stack overflow risk) | Memory fragmentation over time |
Where are variables stored?
Variables are stored on either the stack or heap depending on where they are declared and what type of variables they are.
Value Type Variables
Value-type variables declared in a method are always stored on the call stack along with the method. These are the data types that you typically have for value types.
bool, byte, char, decimal, double, enum, float, int, long, sbyte, short, struct, uint, ulong, short
The exception to this is global variables or variables that are declared in a class will always be on the heap. Global variables need to be accessible across multiple function calls and therefore cannot live on the stack.
Reference-Type Variables
Reference type variables always live on the heap. Reference types are made up of 2 parts:
Pointer to an address in memory.
The actual data value.
When you declare a reference type variable in a method the pointer stays on the call stack with the method and the value is on the heap.
Once the method has finished executing it is popped off the stack and therefore the value on the heap no longer has anything pointing to it. This is where the garbage collector comes in to clear up the memory (assuming you are using something like Java or C#).
Exceptions to where variables are storage
If only everything was as clear-cut as value types on the stack and reference types on the heap.
Where do static variables live?
Static variables even if they are value types are not stored on the stack or the heap. The fact that they are static means they are not dynamically allocated. Static variables are allocated memory at the start of execution and memory isn’t freed until the application has finished execution. Static variables are typically stored in the data segment (the initialized/uninitialised data blocks in the diagram at the start of this post).
Variable access in anonymous functions
C# has anonymous functions which can be created and called from inside another function. The key thing with anonymous functions is that they have access to the local variables from the method that was created and called it.
When the anonymous function is called it is added to the call stack like any other method. Therefore the only way that it can have access to the variables from the calling method is if they are added to the heap.
Result storage from asynchronous methods
When we call asynchronous methods they run on a different thread. Each new thread gets its own call stack which can execute separately from the main thread.
Asynchronous methods can finish executing before the main thread and therefore the return values need to be stored on the heap so the main thread can access them.
When to use Stack or Heap?
In most high-level programming languages the decision of whether to use stack or heap will be made up for you.
However, if you do need to decide which to use then these are the main rules.
- Use the stack when your variable is small and is not going to be used after a method has finished executing.
- Use the heap if the variable is large or needs to be accessed beyond the lifetime of a method.