Stack vs Heap

When a program runs, data must be loaded into memory. The space data occupies and its storage location are determined by its data type. Two memory regions are used to hold runtime data: Stack and Heap.

Stack

The stack is a contiguous memory array built around the LIFO (Last In, First Out) data structure. It stores three categories of data:

  • Values of certain variable types
  • The current execution context of the program
  • Parameters passed into methods
A stack works much like a coin tube: items can only be added or removed from one single end.

Memory operations on the stack are fully automated by the system; developers never need to manage them manually. Grasping the underlying mechanics of the stack makes it far easier to follow program execution flow and read the official C# documentation.

Core Properties of the Stack

  • Data can only be inserted or removed at the stack top
  • Writing data to the stack top: Push
  • Retrieving data from the stack top: Pop

Characteristics of Stack Memory

  • Extremely fast allocation and release, backed by contiguous memory blocks
  • Lifetime tied to method calls: stack frames are automatically destroyed once the method finishes executing
  • Stores: value-type variables, method parameters, local variables, execution context

Let’s walk through a straightforward code sample to better understand how the heap operates

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Code language: JavaScript (javascript)

This sample is fairly simple. Starting from the first line of the entry point Main, we declare an integer variable a, then create variable b on the second line. The third line invokes the Add function and passes the values of a and b into it. Inside Add, x holds the value of a while y holds the value of b. The function returns the sum of x and y, which evaluates to 30 and gets sent back to the caller to assign to sum. Once Add completes, x and y are popped off the stack, sum becomes 30, and after Main finishes, the program exits. We’ll break down each step below to show how the stack handles this full workflow.

Program execution flow & push/pop stack process

When declaring variable a, primitive built-in types are allocated directly on the stack via a push operation
The second line initializes the second integer b with value 20. Earlier entries sit at the stack bottom, newer entries rest at the stack top
The third line runs Add(a,b) on the right side of the assignment. The values of a and b are copied to x and y respectively. Note this is purely value passing; a and x remain entirely separate variables with no linkage between them.
This step only calculates x + y and returns the result, which doesn’t require persistent stack storage. The value 30 is passed back to the caller for assignment to sum. Once return x+y executes, the Add method prepares to terminate. All local variables created inside the function get popped; popping clears their allocated memory space for future reuse.
The uppermost position on the stack is called the Top; after popping, the top marker shifts downward.
After Add finishes, x and y are local parameters belonging to Add and must be popped in sequence to free stack space and maintain smooth system performance.
Once popped, x and y cease to exist. The returned value 30 is delivered back to the caller to assign to sum.
Executing int sum = 30 triggers another push operation, placing sum with value 30 at the stack top. After this statement, Main is ready to exit, and its local variables sum, b and a will soon be popped and destroyed.
Pop operations always remove items starting from the top. A stack acts like a one-way dead end with only one access point for both push and pop. Variables inside Main are destroyed in reverse creation order: sum first, then b, finally a.
Variable b is popped and destroyed, automatically shifting the stack top downwards.
All variables are now popped, including the final variable a. Strictly speaking, Main contains an additional local variable: the string array args from Main(string[] args), which receives command-line arguments passed at console launch. This array stores data split across two memory locations, a concept tied to the heap covered later. We’ll omit args for simplicity in this demo.

Keep this rule in mind: variables are pushed top-to-bottom as functions execute. Our workflow order goes a → b → enter Add → x → y, then Add terminates with x and y popped, followed by pushing sum. When Main exits, sum, b and a are popped in reverse creation order.


The Heap

The heap is a large memory region allocated to store data objects. Unlike the stack, objects on the heap can be added and deleted in any arbitrary order.

Programs cannot manually delete heap objects. The CLR (Common Language Runtime) includes a built-in Garbage Collector (GC) that automatically cleans up orphaned heap objects with no active references. This eliminates error-prone manual memory release required in many other programming languages.

The heap can be visualized as a large desk where you can place items anywhere freely. By contrast, the stack resembles the coin tube we covered earlier, restricted to single-end access only.

The heap is comparable to a wide desk with unrestricted placement of items. Developers never manually delete heap objects; management falls to the GC (Garbage Collector). Once an object loses all references, the GC periodically destroys it in the background. This drastically reduces developer workload and prevents program crashes from unclaimed memory leaks. Languages like C++ force programmers to manually free all allocated heap memory.

GC Collection Workflow

  1. The program instantiates three heap objects and holds active references to each
  2. The program stops using one object, severing all references to it
  3. The GC detects the unreferenced orphan object and releases its occupied memory
  4. Collection completes; freed memory becomes available to allocate new objects

Characteristics of Heap Memory

  • Non-contiguous memory blocks; allocation speed slower than the stack
  • Lifetime independent of method scope, centrally managed by the GC
  • Stores: reference-type instances (classes, arrays, delegates, etc.); the stack only stores memory address pointers pointing to heap objects

Let’s examine another example to demonstrate this clearly. As covered earlier, local primitive value-type variables reside on the stack, while custom user-defined types are allocated on the heap. See the sample below:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    // Member fields
    private string name;

    private int age;

    // Constructor, covered in later chapters
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    // Member method
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Code language: JavaScript (javascript)

The following diagram illustrates memory storage for the four variables a, b, tom and lucy inside Main. Execution proceeds top-down: a gets pushed to the stack first, followed by b. The raw values of a and b are stored directly within stack memory.

Primitive value-type variables are stored directly on the stack
For custom classes like Student, the actual object data lives on the heap. The stack only stores a memory address pointing to the object’s heap location.
In this example, tom’s data resides in heap block 00001, and the stack holds the address 00001 to locate the object data later.

Once Main finishes executing, the address pointer lucy: 01010 is popped off the stack and destroyed. Crucially, only the address reference is removed—not the actual object data stored on the heap. At this point, the heap object loses all stack-based references tracking its memory location.

The Lucy object stored in heap block 01010 now has no stack variables holding its address, leaving it an unreferenced orphan object. The Garbage Collector will destroy this object during its next collection cycle.
The Lucy object on the heap loses all active references once its stack address pointer gets popped. With no remaining references, the GC clears the object’s memory at its convenience to reuse the heap space later. The GC will never delete objects that still hold valid references anywhere in the program.

Memory Segments Overview

Stack and Heap are logical memory divisions implemented at software level, not physical hardware partitions.

Stack and Heap represent purely logical memory splits managed by software; physical RAM hardware contains no dedicated hardware modules labeled “stack area” or “heap area”.

Physical RAM sticks only consist of uniform memory chips with sequential physical addresses, with no built-in markers distinguishing stack versus heap regions.

The operating system allocates an isolated virtual memory address space for every running process, pre-splitting this virtual memory into four core logical segments:

Code Segment: Stores compiled program instructions

Global Static Segment: Holds static variables and constant values

Stack Segment: Each main thread and child thread of the process gets its own independent stack memory block

Heap Segment: A large shared memory pool for dynamic memory allocation

The Stack and Heap discussed here are purely logical regions assigned by the OS to each process; there exists no hardware-level isolation between them.

Stack vs Heap

Leave a Reply

Your email address will not be published. Required fields are marked *