Why Programming Languages Need Garbage Collection: Stack, Heap, and Memory Safety

Arpit Bhayani

Arpit Bhayani

Apr 11, 2022 • 7 min read

Play

Every stateful program requires physical memory to operate. Whether computing a simple arithmetic sum, managing complex structs, displaying user interface components, or handling state in a web application, variables must be allocated space in RAM. In modern computer architecture, this memory is categorized into two primary storage areas: the Stack and the Heap.

While stack memory is managed deterministically and automatically by the CPU architecture, heap memory requires deliberate lifecycle handling. This necessity forms the foundation of modern garbage collection.


1. The Duality of Memory: Stack vs. Heap

+-------------------------------------------------------------+
|                      RAM Storage Space                      |
|                                                             |
|  +--------------------+         +------------------------+  |
|  |    Stack Memory    |         |      Heap Memory       |  |
|  |--------------------|         |------------------------|  |
|  | LIFO Frames        |         | Dynamic allocations    |  |
|  | Scoped to Function |         | Outlives function scope|  |
|  | Fixed, small size  |         | Arbitrary growth       |  |
|  | Auto-reclaimed     |         | Shared across threads  |  |
|  +--------------------+         +------------------------+  |
+-------------------------------------------------------------+

Stack Memory Allocation

When a function executes, it pushes a stack frame containing its local variables onto the call stack:

void compute() {
    int a = 10; // Allocated in compute's stack frame
}
  • Automatic Lifecycle: When compute() invokes another function, a new frame is pushed. When the function returns, the CPU stack pointer drops, popping the frame. All variables within that frame immediately lose relevance and are reclaimed with virtually zero runtime cost.
  • Limitations: The stack has a strictly limited size (typically a few megabytes per thread). Exceeding this boundary triggers a stack overflow. Furthermore, stack-allocated data cannot safely outlive the function that created it, nor can it efficiently support dynamic, runtime-determined sizing.

Heap Memory Allocation

The heap comprises the non-stack portion of available RAM. Programs explicitly request contiguous byte segments from the language runtime or operating system kernel (e.g., via malloc() in C or new in C++):

struct Book {
    char title[100];
};

// Allocate space for 10 books (10 * 100 = 1000 bytes) on the heap
struct Book *books = (struct Book *)malloc(10 * sizeof(struct Book));

Here, the variable books on the stack stores only an address (a memory pointer) pointing to the first byte of the 1000-byte block allocated on the heap.


2. Why We Need the Heap

If stack allocation is fast and self-cleaning, why not allocate everything on the stack? Heap allocation is indispensable for three architectural reasons:

A. Storing Large Objects

Allocating massive structures (e.g., a 100 MB buffer or thousands of records) on the stack will quickly exhaust the thread stack limit, causing an immediate crash. The heap provides an expansive pool capable of accommodating large allocations without compromising execution frames.

B. Dynamic, Unbounded Data Structures

Structures such as dynamically-sized arrays, linked lists, and trees cannot be sized at compile time:

[Node A] ---> [Node B] ---> [Node C] ---> [Node D (dynamically allocated)]

A program may start with one node and scale to millions based on runtime traffic. Because each node can be allocated on-demand in the heap and linked via pointers, the data structure can grow without static bounds.

C. Cross-Function and Cross-Thread Sharing

Stack variables are destroyed when their enclosing function terminates. If a variable must outlive the function scope, or if multiple functions and concurrent threads need to reference and mutate the same shared state without copying massive objects by value, the object must reside in the heap.


3. Explicit Memory Deallocation and Human Fallibility

In unmanaged environments (such as C or C++), languages provide explicit APIs to release heap memory back to the allocator (free(ptr) or delete ptr):

1. malloc()  ===> Allocates chunk at Address 0x00FF
2. Program   ===> Operates on 0x00FF
3. free()    ===> Returns 0x00FF to the free-list for future reuse

Explicit deallocation relies entirely on the programmer to account for every execution path. In production systems with branching logic, exception handling, and concurrent threads, manual deallocation inevitably leads to critical bugs.

graph TD
    A[Allocate Memory on Heap] --> B{Execution Flow}
    B -->|Path 1: Success| C[Call free/delete]
    B -->|Path 2: Early Return/Exception| D[Missed free -> Memory Leak]
    B -->|Path 3: Deallocated while still referenced| E[Dangling Pointer / Use-After-Free]

4. The Two Disastrous Pitfalls of Manual Memory Management

Pitfall 1: Memory Leaks (Failure to Deallocate)

A memory leak occurs when heap memory is allocated, is no longer needed or referenced by any active component of the program, but is never released back to the runtime allocator.

  • Mechanism: Over time, uncollected memory blocks accumulate in the heap. The process’s Resident Set Size (RSS) steadily climbs.
  • Consequence: When memory utilization reaches 100%, subsequent calls to malloc or memory allocators fail, triggering out-of-memory (OOM) panics or process termination by the OS kernel OOM-killer.
Initial State:  [ Used ][ Free                                       ]
Under Leak:     [ Leaked ][ Leaked ][ Leaked ][ Used ][ Out of Memory! ]

Pitfall 2: Dangling Pointers and Use-After-Free

A dangling pointer occurs when an object is explicitly deallocated while another reference or pointer to that address continues to exist.

sequenceDiagram
    participant Thread A
    participant Heap Memory (0xABCD)
    participant Thread B

    Thread A->>Heap Memory (0xABCD): Allocate & use object
    Thread A->>Heap Memory (0xABCD): free(0xABCD)
    Note over Heap Memory (0xABCD): 0xABCD marked as available in allocator
    Thread B->>Heap Memory (0xABCD): malloc() claims 0xABCD, writes new data
    Thread A->>Heap Memory (0xABCD): Reads/Writes to dangling reference!
    Note over Thread A,Thread B: Silent data corruption / Non-deterministic crashes

Why Dangling Pointers Are Worse Than Crashes

When Thread A accesses a dangling pointer to 0xABCD after Thread B has claimed that memory:

  1. The Best-Case Scenario: The OS detects an invalid memory access or segmentation fault, and the process crashes immediately, leaving a core dump for debugging.
  2. The Worst-Case Scenario: The process does not crash. Thread A reads arbitrary bytes written by Thread B, interpreting them as valid business logic, or Thread A overwrites Thread B’s memory. This leads to silent data corruption, non-deterministic application behavior, and severe security vulnerabilities.

5. The Solution: Automatic Garbage Collection

Automatic Garbage Collection (GC) eliminates the requirement for explicit deallocation by delegating heap lifecycle management to the programming language runtime.

+-------------------------------------------------------------+
|                   Why Systems Use Auto GC                   |
|-------------------------------------------------------------|
| 1. High Reliability: Memory lifecycle governed by runtime.  |
| 2. Cognitive Relief: Eliminates manual allocation tracking. |
| 3. Eliminates Human Error: Prevents use-after-free bugs.    |
+-------------------------------------------------------------+

Key Concepts in Automatic Collection

Modern runtimes implement sophisticated strategies to identify unreferenced objects and compact memory:

  • Reference Counting: Tracks the number of references pointing to each object; reclaims memory when the count reaches zero (subject to cyclic reference challenges).
  • Mark and Sweep: Traverses object graphs starting from root references (stack frames, global registers). Unreachable objects are swept and reclaimed.
  • Generational Collection (Young vs. Old Generation): Based on the weak generational hypothesis (most objects die young), segmenting the heap into Eden, Survivor, and Tenured spaces allows for optimized, low-latency collection passes.
  • Concurrent and Region-Based Collectors: Advanced engines (such as Java’s G1 GC or ZGC, Go’s concurrent collector) minimize Stop-The-World (STW) pauses, allowing applications to maintain predictable latency profiles.

Summary

Memory management requires choosing between manual control and runtime safety. While manual memory management provides low-level control, the cognitive overhead and structural risks of memory leaks and dangling pointers make it impractical for most modern software engineering. Automatic garbage collection shifts these guarantees to the runtime engine, ensuring predictable, memory-safe execution across complex architectures.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses