[← BACK_TO_INDEX]
May 24, 20267 min read

Understanding Memory Management: Stack vs. Heap, Pointers, and Garbage Collection

Memory Management

Every time your program runs, it works with data. It creates variables, reads user input, processes numbers, and returns outputs. But have you ever wondered: where does all this data live?

The short answer is Memory (RAM). But the long answer is far more interesting. How does a computer know where to put a variable? When does it decide that a variable is no longer needed and clean it up?

Here at Curious Folk, we believe in breaking down complex systems. In this article, we will demystify the complex world of memory management. We’ll explore the difference between the Stack and the Heap, see how Pointers navigate memory, understand how languages automatically clean up using Garbage Collection, and look at how modern systems like Rust ensure memory safety without runtime overhead.


The Restaurant Analogy

Before we dive into technical details, let’s look at an everyday analogy. Imagine you run a busy restaurant:

  1. The Stack (Fast Food Counter): Customers walk up to the counter, order a burger, eat it immediately, and leave. The orders are processed quickly, and as soon as a customer is done, their spot is immediately cleared for the next person. It requires minimal management, is extremely fast, but is highly structured and limited in space.

  2. The Heap (Main Dining Room): A group of 8 guests arrives without a booking. They need a custom-sized table set up. You must search the dining room for a large enough empty space, move tables together, seat them, and keep track of their bill. They might stay for hours. Once they leave, you must clean the table and make that space available for new guests. This is flexible and spacious, but it takes time to find a spot and clean up afterwards.

Now, let’s translate this analogy to computer hardware.


Memory Layout of a Program

When an operating system runs your compiled program, it allocates a chunk of Virtual Memory. This memory is typically divided into several regions:

Memory Region Description
Text/Code Segment Stores the compiled machine instructions (the code itself). Read-only.
Data Segment Stores global and static variables that exist for the entire lifespan of the program.
Stack Stores temporary local variables and function call execution frames. Grows downward.
Heap Stores dynamically allocated memory. Grows upward.
  +-------------------------+  Low Memory Address
  |      Code Segment       |
  +-------------------------+
  |      Data Segment       |
  +-------------------------+
  |          Stack          |  (Grows downward)
  |            |            |
  |            v            |
  |                         |
  |            ^            |
  |            |            |
  |          Heap           |  (Grows upward)
  +-------------------------+  High Memory Address

1. The Stack: Fast and Ordered

The Stack is a region of memory that operates on a Last In, First Out (LIFO) basis. It stores local variables and handles function call execution.

How Stack Frames Work

Every time a function is called, the CPU creates a new block of memory on the stack called a Stack Frame (or Activation Record). This frame stores:

  • The parameters passed to the function.
  • The return address (where the CPU should go back to when the function finishes).
  • Local variables declared inside the function.

When the function finishes, its stack frame is popped off. The memory is instantly reclaimed by simply moving a CPU pointer (the Stack Pointer).

Example in C:

#include <stdio.h>

void calculateSum(int a, int b) {
    int sum = a + b; // Allocated on calculateSum's stack frame
    printf("Sum: %d\n", sum);
} // calculateSum frame popped here! memory reclaimed.

int main() {
    int x = 10; // Allocated on main's stack frame
    int y = 20; // Allocated on main's stack frame
    
    calculateSum(x, y);
    
    return 0;
}

Visualizing the Stack Lifecycle:

  1. main starts: Stack Pointer moves to allocate space for x and y.
  2. calculateSum is called: A new frame is pushed above main. Space is allocated for a, b, and sum.
  3. calculateSum ends: The frame is popped. The Stack Pointer moves back. The memory for sum is gone.
  4. main ends: The main frame is popped. The program exits.

Pros and Cons of Stack Allocation:

  • Pros:
    • Incredibly Fast: Memory allocation is a single CPU instruction (moving the stack pointer).
    • Auto-Cleanup: No manual deletion is needed. Variables are cleaned up as soon as they go out of scope.
    • Data Locality: Stack memory is close together, making it highly cache-friendly.
  • Cons:
    • Limited Size: The stack is relatively small (typically 1MB to 8MB). Allocating too much data causes a Stack Overflow.
    • Fixed Size Requirements: You must know the exact size of variables at compile time.
    • Scope-Bound: You cannot return a reference to a stack-allocated variable from a function because that memory is destroyed when the function returns.

2. The Heap: Flexible and Dynamic

What if you need to load a 100MB image file, but you don’t know the file size until the user clicks “Open”? Or what if you want to create a list of data that persists even after the function creating it finishes?

This is what the Heap is for. The Heap is a large, unorganized pool of memory.

Dynamic Memory Allocation

Unlike the stack, you must explicitly request memory on the heap. In languages like C/C++, you do this manually.

Example in C:

#include <stdio.h>
#include <stdlib.h> // Required for malloc and free

int* createArray(int size) {
    // Request memory on the Heap for 'size' integers
    int* arr = (int*)malloc(size * sizeof(int));
    
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return NULL;
    }
    
    for (int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }
    
    return arr; // Returning pointer; heap memory persists!
}

int main() {
    int* myData = createArray(5);
    
    for (int i = 0; i < 5; i++) {
        printf("%d ", myData[i]);
    }
    printf("\n");
    
    // Crucial step: Freeing the allocated memory
    free(myData);
    
    // Prevent dangling pointer
    myData = NULL; 
    
    return 0;
}

The Concept of Pointers

Notice that createArray returned int* arr. That asterisk * denotes a pointer. A pointer is simply a variable that stores the memory address of another value (usually on the heap).

   Stack Variable (myData)             Heap Memory Address
   +--------------------+              +---------------+
   | Address: 0x7ffd01  | -----------> | Index 0: 0    |
   | Value:   0x9a88c0  |              | Index 1: 10   |
   +--------------------+              +---------------+

If you forget to call free(myData) in C, that heap memory is never returned to the system while your program runs. This is called a Memory Leak. If a long-running server leaks memory continuously, it will eventually run out of RAM and crash.

Pros and Cons of Heap Allocation:

  • Pros:
    • Dynamic Size: You can allocate memory of any size at runtime.
    • Lifetime Control: The data persists until you choose to delete it.
    • Large Capacity: Limited only by the physical RAM available on the machine.
  • Cons:
    • Slower: Finding an empty block of memory on the heap is slow compared to the stack.
    • Fragmentation: Over time, allocating and freeing random chunks of memory leaves “holes,” making it harder to find large contiguous blocks.
    • Manual Responsibility: In C/C++, you must clean up manually. Failure to do so leads to leaks, or “Use-After-Free” bugs.

3. Automatic Cleanup: Garbage Collection (GC)

Writing malloc and free for every single variable is tedious and error-prone. To solve this, high-level languages like JavaScript, Python, Java, and Go use a runtime component called a Garbage Collector (GC).

The Garbage Collector automatically detects when heap memory is no longer reachable by the program and frees it.

There are two main algorithms used for Garbage Collection:

A. Reference Counting

This is the simplest form of GC (used in Python).

  • Every object in the heap keeps track of how many pointers/references point to it.
  • When you create a reference, the count goes up.
  • When a reference goes out of scope or is overwritten, the count goes down.
  • If the count hits zero, the object is immediately destroyed.

Example:

let user = { name: "Alice" }; // Object A created. Reference count = 1.
let administrator = user;     // Reference count = 2.

user = null;                  // Reference count = 1.
administrator = null;         // Reference count = 0! Object A is garbage collected.

The Circular Reference Problem:

Reference counting has a major flaw: Circular References.

function makeCycle() {
    let objectA = {};
    let objectB = {};
    
    objectA.friend = objectB; // A points to B
    objectB.friend = objectA; // B points to A
}
makeCycle();

When makeCycle ends, objectA and objectB go out of stack scope. However, they still reference each other. Their reference counts remain at 1, meaning they will never be garbage collected, creating a memory leak.

B. Mark-and-Sweep

To fix circular references, modern runtimes (like JavaScript’s V8 engine) use Mark-and-Sweep.

Instead of tracking count, this algorithm periodically runs a search starting from the Root (global variables, active local variables on the call stack).

It works in two phases:

  1. Mark Phase: The GC traverses the memory graph starting from the roots. Every object it can reach is marked as “active/reachable.”
  2. Sweep Phase: The GC scans all heap objects. Any object that was not marked as reachable during the mark phase is destroyed.
       [ Stack Root ]
             |
             v
         [Object A] <---> [Object B]      [Object C] (Circularly references itself)
                                               ^
                                               |
                                          [Object D]

In the diagram above, Object C and Object D are not reachable from the stack root. Even though they reference each other, they will be swept away by the GC.

Drawbacks of Garbage Collection:

  • Stop-the-World pauses: The garbage collector needs to pause code execution occasionally to clean up memory, which can cause frame drops in games or lag in real-time applications.
  • Memory Overhead: Runtimes require extra memory to track allocations and graph structures.

4. The Modern Way: Rust’s Ownership and Borrowing

What if we could have the safety of a Garbage Collector without the runtime performance penalty?

This is the breakthrough of the Rust programming language. Rust manages memory using a set of compile-time rules called Ownership:

  1. Each value in Rust has an owner (a variable).
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is automatically dropped (freed).

Let’s see how Rust handles heap allocation:

fn create_string() {
    // Dynamic string allocated on the heap
    // 's' is the owner of the heap allocation
    let s = String::from("Hello, Cyberpunk World!"); 
    
    println!("{}", s);
} // 's' goes out of scope here. 
  // Rust compiler automatically inserts code to free the heap memory!

Since the compiler knows exactly when a variable is no longer needed, it inserts the cleanup code (deallocate) during compilation. No Garbage Collector is needed, and no manual free is required.

Borrowing and References

To allow sharing data without duplicating memory, Rust uses Borrowing:

  • Immutable Borrow (&T): You can have multiple read-only references to a value.
  • Mutable Borrow (&mut T): You can have only one writeable reference to a value at a time (preventing data races).
fn print_length(str_ref: &String) { // Borrowing string read-only
    println!("Length: {}", str_ref.len());
}

fn main() {
    let s = String::from("Rust");
    print_length(&s); // Pass reference
    // 's' is still valid here because we only lent it!
}

Summary Cheat Sheet

Here is a quick comparison table to help you remember the key differences:

Feature The Stack The Heap
Access Speed Extremely fast (direct CPU pointer shift) Slower (requires lookup and allocation logic)
Size Limit Small (typically 1MB - 8MB) Very large (limited by system RAM)
Lifecycle Tied to the function scope (LIFO) Dynamic (persists until deleted or garbage collected)
Control Automatic by CPU / compiler Manual (C/C++), Automatic (JS, Python, Rust)
Data Types Primitive values, fixed-size pointers Dynamic structures, arrays, objects, classes
Risk Stack Overflow Memory Leaks, Dangling Pointers, Bloat

Understanding these concepts is the key to writing memory-efficient software, diagnosing memory leaks, and understanding how programming languages work under the hood.

The next time you declare a variable or query an API, think about the silent ballet of Stack pointers and Heap allocations making it all happen! 🚀