On this page:
11.1 Memory Attributes
11.2 Memory Classes
11.2.1 Static Memory
11.2.2 Stack (LIFO) Memory
11.2.3 Dynamic (Heap) Memory
11.3 Manual vs. Automatic Recovery
11.3.1 Manual Memory Management
11.3.2 Automatic Memory Management (Garbage Collection)
11.4 Memory Management in Practice
11.4.1 Memory Management in C
11.4.2 Memory Management in Java
11.4.3 Memory Management in OCaml
11.5 Automatic Memory Management
11.5.1 Fragmentation
11.5.2 Defragmentation
11.6 GC Strategy:   Live vs. Dead Objects
11.6.1 Determining Liveness
11.6.2 Liveness by Reachability
11.6.2.1 Roots
11.7 Reference Counting
11.7.1 Step-by-Step Walkthrough
11.7.2 Where Reference Counting Is Used
11.7.3 Rust Rc Example
11.7.4 Reference Counting Tradeoffs
11.8 Tracing Garbage Collection
11.9 Mark and Sweep GC
11.9.1 Mark and Sweep Advantages
11.9.2 Mark and Sweep Disadvantages
11.10 Copying GC
11.10.1 Copying GC Tradeoffs
11.11 Stop-the-World Pause
11.12 Generational Collection
11.12.1 Java Hot  Spot SDK 1.4.2 Collector
11.13 Conservative Garbage Collection
11.14 Comparison of GC Strategies
11.15 Performance Tips
11.16 Common Memory Leak Example
11.17 Summary
9.0

11 Memory Management and Garbage Collection🔗

    11.1 Memory Attributes

    11.2 Memory Classes

    11.3 Manual vs. Automatic Recovery

    11.4 Memory Management in Practice

    11.5 Automatic Memory Management

    11.6 GC Strategy: Live vs. Dead Objects

    11.7 Reference Counting

    11.8 Tracing Garbage Collection

    11.9 Mark and Sweep GC

    11.10 Copying GC

    11.11 Stop-the-World Pause

    11.12 Generational Collection

    11.13 Conservative Garbage Collection

    11.14 Comparison of GC Strategies

    11.15 Performance Tips

    11.16 Common Memory Leak Example

    11.17 Summary

Every programming language must confront a fundamental question: how is memory allocated, used, and reclaimed? In languages like C, programmers manually manage heap memory with malloc and free. In languages like OCaml, Java, and Ruby, this burden is lifted through automatic memory management, also called garbage collection.

This lecture examines how memory is classified, why automatic reclamation is desirable, and the major algorithms used to identify and reclaim unreachable objects.

11.1 Memory Attributes🔗

Memory used to store data in a program has a lifecycle with three stages:

  • Allocation — when the memory is allocated to the program

  • Lifetime — how long the allocated memory is used by the program

  • Recovery — when the system recovers the memory for reuse

The system component that performs allocation and recovery is called the allocator.

Most programming languages are concerned with three memory classes:

  1. Static (fixed) memory

  2. Stack (LIFO) memory

  3. Dynamically allocated (heap) memory

11.2 Memory Classes🔗

11.2.1 Static Memory🔗

Static memory resides at a fixed location for the entire duration of program execution.

  • Lifetime — lasts for the entire program execution

  • Allocation — occurs once, before or at program start

  • Allocator — handled by the compiler

  • Recovery — automatically reclaimed when the program terminates

11.2.2 Stack (LIFO) Memory🔗

Stack memory operates in a last-in, first-out (LIFO) manner and is closely tied to function or method calls.

  • Lifetime — limited to the duration of a function/method invocation

  • Allocation — occurs when a function is called

  • Allocator — usually managed by the compiler (sometimes influenced by the programmer)

  • Recovery — automatically occurs when the function returns

The stack typically stores:
  • Local variables

  • Function parameters

  • Return addresses

11.2.3 Dynamic (Heap) Memory🔗

Dynamic memory is allocated at runtime from a region called the heap.

  • Lifetime — persists as long as the program needs it

  • Allocation — performed explicitly by the programmer or implicitly by the runtime/compiler

  • Allocator — manages available space within the heap

  • Recovery — handled either:
    • Manually (e.g., free in C)

    • Automatically via garbage collection

11.3 Manual vs. Automatic Recovery🔗

There are two main approaches to reclaiming heap memory, each with clear advantages and tradeoffs.

11.3.1 Manual Memory Management🔗

In manual memory management, the programmer is responsible for explicitly allocating and freeing memory.

Characteristics:
  • Efficient — minimal overhead, uses memory more tightly

  • Error-prone — easy to introduce bugs such as:
    • Memory leaks (forgetting to free memory)

    • Dangling pointers (using memory after it’s freed)

Example (C):

#include <stdlib.h>

int main() {
    int *p = (int*) malloc(sizeof(int)); // allocate memory
    *p = 10;

    // forgot to free(p); → memory leak
    return 0;
}

Use-after-free example (dangerous):

free(p);
*p = 20;  //undefined behavior (dangling pointer)

These mistakes can lead to crashes or serious security vulnerabilities.

11.3.2 Automatic Memory Management (Garbage Collection)🔗

In automatic memory management, the system/runtime handles memory reclamation.

Characteristics:
  • Less efficient — adds overhead (extra memory + occasional pauses)

  • Easier and safer — no need to track when objects should be freed

  • Avoids common bugs like leaks and dangling pointers

Example (Java):

public class Main {
    public static void main(String[] args) {
        String s = new String("hello"); // allocated on heap
        
        s = null; // object becomes unreachable
        // garbage collector will reclaim it automatically
    }
}

Example (Python):

def func():
    x = [1, 2, 3]  # allocated
    return

func()
# x is no longer reachable → automatically cleaned up

In modern software, automatic memory management is preferred for most applications, especially where safety and developer productivity matter.

11.4 Memory Management in Practice🔗

11.4.1 Memory Management in C🔗

In C, local variables live on the stack:

  • Allocated at function invocation time

  • Deallocated when the function returns

  • Storage space is reused after the function returns

Space on the heap is allocated with malloc() and must be explicitly freed with free(). This is called explicit or manual memory management — deletions must be performed by the programmer.

11.4.2 Memory Management in Java🔗

In Java, local variables also live on the stack, and storage is reclaimed when a method returns. However, objects live on the heap:

  • Created with calls to Class.new (or equivalent)

  • Objects are never explicitly freed — the runtime uses automatic memory management (garbage collection)

11.4.3 Memory Management in OCaml🔗

In OCaml, local variables live on the stack, but tuples, closures, and constructed types live on the heap:

let x = (3, 4)       (* heap-allocated *)
let f x y = x + y in f 3  (* result heap-allocated *)
type 'a t = None | Some of 'a
None      (* not on the heap — just a primitive *)
Some 37   (* heap-allocated *)

Data on the OCaml heap is reclaimed via garbage collection automatically.

11.5 Automatic Memory Management🔗

The primary goal of automatic memory management is to automatically reclaim dynamic memory. A secondary goal is to avoid fragmentation — the situation where free memory is scattered in small, non-contiguous pieces throughout the heap.

The key insight is: if you can identify every pointer in a program, you can both reclaim dead memory and compact the live data to avoid fragmentation. Compaction works by moving allocated objects and redirecting all pointers to their new locations.

Languages like LISP, OCaml, Java, and Prolog give the compiler perfect knowledge of which values are pointers. Languages like C, C++, Pascal, and Ada do not, making this approach difficult or impossible.

11.5.1 Fragmentation🔗

Fragmentation is a serious problem independent of GC. It happens when free memory exists, but it is split into small pieces that are not usable for a single allocation. This is especially important in heap allocation systems without compaction.

Let’s walk through this example step by step. Assume we start with a contiguous memory block:

Memory (start → end)

+----+----+----+----+----+
|    |    |    |    |    |
+----+----+----+----+----+

We allocate:

allocate(a); allocate(x); allocate(y);

Result:

+----+----+----+----+----+
| a  | x  | y  |    |    |
+----+----+----+----+----+

Now we free a:

free(a);

+----+----+----+----+----+
|    | x  | y  |    |    |
+----+----+----+----+----+

Now we free x:

free(x);

+----+----+----+----+----+
|    |    | y  |    |    |
+----+----+----+----+----+

At this point, memory is fragmented: there are two free spaces, but they are separated. Now we try to allocate b, which needs 3 contiguous blocks:

allocate(b);  (* fails *)

11.5.2 Defragmentation🔗

Defragmentation is the process of reorganizing memory so that free space becomes contiguous again, instead of being split into small scattered holes. It moves allocated blocks together and merges free space into one large block.

Before defragmentation
+----+----+----+----+----+----+
| a  |    | x  |    | y  |    |
+----+----+----+----+----+----+

Defragmentation shifts live objects together:


After defragmentation (compaction)
+----+----+----+----+----+----+
| a  | x  | y  |    |    |    |
+----+----+----+----+----+----+
Now all free memory is contiguous.

11.6 GC Strategy: Live vs. Dead Objects🔗

At any point during execution, the objects in the heap can be divided into two classes:

  • Live objects will be used later

  • Dead objects will never be used again — they are "garbage"

We need garbage collection (GC) algorithms that can:

  1. Distinguish live from dead objects

  2. Reclaim the dead objects and retain the live ones

11.6.1 Determining Liveness🔗

In most languages, we cannot know for certain which objects are truly live or dead at runtime. This is undecidable — analogous to solving the halting problem.

Therefore, GC must make a safe approximation:

  • It is safe to decide something is live when it is not (conservative overapproximation)

  • It is not safe to deallocate an object that will be used later

11.6.2 Liveness by Reachability🔗

The standard safe approximation is reachability. An object is reachable if it can be accessed by following ("chasing") pointers from live data.

The safe policy is: delete unreachable objects.

  • An unreachable object can never be accessed again by the program — it is definitely garbage

  • A reachable object may be accessed in the future — it could be garbage but will be retained anyway, which can lead to memory leaks

11.6.2.1 Roots🔗

Liveness is defined as data reachable from the root set:

  • Global variables — static fields, module-level bindings (e.g., in Java, Ruby, OCaml)

  • Local variables of all live method activations — i.e., the stack

  • At the machine level, also the register set, which typically stores local or global variables

The goal of every GC algorithm is to trace reachability starting from these roots.

11.7 Reference Counting🔗

The first major approach to GC is reference counting, invented by Collins in 1960 ("A method for overlapping and erasure of lists," Communications of the ACM, December 1960).

Idea: Each object maintains a count of the number of pointers to it from roots or other objects. When the count reaches 0, the object is unreachable and can be reclaimed.

If we remove external references:

root = null
x = null

Then:

A becomes 0 → freed
B becomes 0 → freed
C becomes 0 → freed

Everything is correctly reclaimed. Count tracking can be manual or automatic.

11.7.1 Step-by-Step Walkthrough🔗

Step 1: Initial Allocation (root → A)

A single object A is allocated. The variable root points to A.

Reference counts:
A = 1  (from root)

Only one object exists, and it is reachable.

Step 2: Allocating B (A → B)

A new object B is created. A now holds a reference to B. The object graph starts to grow.

Reference counts:
A = 1  (from root)
B = 1  (from A)

Step 3: Shared Reference (x → B)

A new variable x is introduced that also points to B, demonstrating shared ownership — multiple references to the same object.

Reference counts:
B = 2  (from A and x)

Step 4 — Allocating C (B → C)

A new object C is allocated. B points to C. We now have the chain root → A → B → C, plus x → B.

Reference counts:
C = 1  (from B)

Step 5 — Removing root (A becomes unreachable)

root is set to null. A loses its only incoming reference:

  • A’s count drops to 0 → freed immediately

  • B retains 1 reference (from x) — not freed

  • C retains 1 reference (from B) — not freed

Freeing A does not free B because B is still referenced by x.

Step 6 — Removing x (B loses its external reference)

x is set to null. B loses one incoming reference.

  • B’s count drops from 1 to 0 → freed

Step 7 — Cascade Deletion (B → C freed)

When B’s reference count drops to 0 and B is freed, its outgoing reference to C is removed as well.

  • B is freed

  • B’s reference to C is released

  • C’s count drops to 0 → freed

This triggers a cascade: freeing one object automatically frees all objects reachable only through it.

11.7.2 Where Reference Counting Is Used🔗

Reference counting is used in:
  • C++ and Rust (smart pointers like Rc<T> and Arc<T>)

  • Cocoa/Objective-C (manual reference counting)

  • Python (automatic reference counting)

11.7.3 Rust Rc Example🔗

Rust’s Rc<T> type provides reference-counted shared ownership:

use std::rc::Rc;
fn main() {
    let s = String::from("hello");
    let r1 = Rc::new(&s);
    {
        let r2 = Rc::clone(&r1);
        println!("r1 = {}", Rc::strong_count(&r1));  // r1 = 2
        println!("r2 = {}", Rc::strong_count(&r2));  // r2 = 2
    }
    // r2 is out of scope
    println!("r1 = {}", Rc::strong_count(&r1));      // r1 = 1
}

When r2 goes out of scope, its reference is dropped and the count decrements to 1. When r1 goes out of scope, the count reaches 0 and the allocation is freed.

11.7.4 Reference Counting Tradeoffs🔗

Advantages:

  • Incremental technique — generally a small, constant amount of work per memory write; with more effort, running time can even be bounded

  • Immediate reclamation — objects are freed as soon as their count drops to 0

Disadvantages:

  • Cascading decrements can be expensive — freeing one object may trigger a chain of reference count decrements

  • Requires extra storage for reference counts

  • Cannot collect cycles — if a set of objects form a cycle, their counts never reach 0 even when they are unreachable from the roots; a separate cycle-detection mechanism is needed

11.8 Tracing Garbage Collection🔗

The second major approach is tracing GC. Rather than maintaining counts incrementally, tracing GC determines reachability on demand.

Idea: Every so often, stop the program ("stop the world") and:

  1. Follow pointers from live objects, starting at the roots, to expand the live object set (repeat until no more reachable objects are found)

  2. Deallocate any non-reachable objects

The two main variants of tracing GC are mark/sweep (McCarthy 1960) and stop-and-copy (Cheney 1970).

11.9 Mark and Sweep GC🔗

Mark and sweep has two phases:

  • Mark phase — trace the heap and mark all reachable objects

  • Sweep phase — go through the entire heap and reclaim all unmarked objects

Each object stores a single mark bit. During the mark phase, the GC traverses all pointers from the root set, setting the mark bit on every reachable object. During the sweep phase, the GC linearly scans the entire heap and places any object whose mark bit is 0 onto the free list.

11.9.1 Mark and Sweep Advantages🔗

  • No problem with cycles — reachability-based tracing handles cyclic structures correctly

  • Non-moving — live objects stay in place, which makes conservative GC possible (used when it is uncertain whether a value is a pointer or an integer, e.g., in C)

11.9.2 Mark and Sweep Disadvantages🔗

  • Fragmentation — available space may be broken up into many small pieces; many mark-and-sweep implementations add a compaction phase (like defragmenting a disk)

  • Cost proportional to heap size — the sweep phase traverses the entire heap, touching dead memory in order to put it back on the free list

11.10 Copying GC🔗

Copying GC (also called stop-and-copy) addresses the sweep-phase cost of mark and sweep by only touching live objects.

Idea: Divide the heap into two equal halves called semispaces. Only one semispace is active at a time. At GC time:

  1. Trace the live data starting from the roots

  2. Copy live data into the other (inactive) semispace

  3. Declare everything remaining in the current semispace dead

  4. Switch to the other semispace

The copying process naturally compacts the live objects, eliminating fragmentation.

11.10.1 Copying GC Tradeoffs🔗

Advantages:

  • Only touches live data — cost is proportional to the amount of live data, not heap size

  • No fragmentation — copying automatically compacts live objects, which tends to increase cache locality

Disadvantages:

  • Requires twice the memory space — only half the heap is usable at any time

11.11 Stop-the-World Pause🔗

Both mark and sweep and copying GC "stop the world" — they prohibit program execution during the collection. This ensures that previously processed memory is not changed or accessed, avoiding inconsistency.

However, the execution pause could be unacceptably long. For example, a car’s braking system performing GC while you are trying to stop at a busy intersection would be catastrophic.

Solutions to reduce pause time:
  • Incremental GC — collect a small portion of the heap per GC step instead of all at once

  • Concurrent GC — run the collector concurrently with the program, using write barriers to track changes

11.12 Generational Collection🔗

The generational principle is an empirical observation: "Young objects die quickly; old objects keep living." Most objects are short-lived, while a small fraction survive for a long time.

Long-lived objects would be visited (and needlessly re-scanned) many times under a flat GC. The solution is to divide the heap into generations:

  • Older generations are collected less frequently

  • Objects that survive many collections are promoted into older generations

  • Pointers from old to young generations must be tracked (in the remembered set) to use as roots when collecting the young generation

One popular setup: generational, copying GC — use copying GC on the young generation (cheap, since most objects die young) and a different strategy for older generations.

11.12.1 Java HotSpot SDK 1.4.2 Collector🔗

Java’s HotSpot JVM uses a multi-generational, hybrid collector:

  • Young generation — stop-and-copy collector

  • Tenured generation — mark and sweep collector

  • Permanent generation — no collection (stores class metadata)

11.13 Conservative Garbage Collection🔗

For languages like C, the GC cannot be certain which elements of an object are pointers. Reasons include incomplete type information, unsafe casts, and type punning.

The conservative approach: suppose a value is a pointer if it looks like one.

  • Most pointers are within a certain address range and are word-aligned

  • If a value has pointer-like properties, treat it as a pointer

  • May retain memory spuriously — a value that looks like a pointer but is actually an integer could keep an object alive

Conservative collectors come in two flavors:

  • Mark-sweep — important that objects are not moved, since we may be wrong about what is a pointer

  • Mostly-copying — can move objects that we are certain are only referenced by known pointers

11.14 Comparison of GC Strategies🔗

The three main approaches differ in several key dimensions:

Method

Handles Cycles

Fragmentation

Memory Use

Cost Basis

Reference Counting

No

Yes

Low

Incremental

Mark and Sweep

Yes

Yes

Medium

Heap size

Copying GC

Yes

No

High (2×)

Live data only

In short: reference counting is cheap per operation but cannot handle cycles; mark and sweep handles cycles but traverses the whole heap; copying GC is fast relative to live data but doubles memory usage.

11.15 Performance Tips🔗

When working with garbage-collected runtimes, a few practices help the GC work more efficiently:

  • Reduce allocations — fewer live objects means less GC work; reuse objects where possible

  • Release references early — setting a reference to null (or reassigning it) allows the GC to reclaim the object sooner rather than waiting for the variable to go out of scope

For example, in Java:

a = null;  // allows a's referent to be collected

11.16 Common Memory Leak Example🔗

Even in garbage-collected languages, memory leaks are possible when live references are unintentionally retained. A classic example is an unbounded stack implemented with an array:

public Object pop() {
    return stack[index--];  // BUG: stale reference remains in array
}

The popped object is no longer logically part of the stack, but stack[index+1] still holds a reference to it. The GC cannot reclaim it because the array (which is reachable) still points to the object.

The fix is to null out the slot after decrementing:

public Object pop() {
    Object obj = stack[index];
    stack[index--] = null;  // eliminate stale reference
    return obj;
}

This pattern illustrates that reachability is a safe approximation of liveness — a reachable object may no longer be needed, but the GC cannot know that.

11.17 Summary🔗

Memory management is a fundamental concern in language design and runtime implementation:

  • GC simplifies programming by removing the burden of manual deallocation, but adds overhead

  • No single GC algorithm is universally best — there are trade-offs between speed, memory usage, pause time, and correctness

  • Modern runtimes use hybrid approaches: generational collection with copying GC for the young generation and mark-and-sweep for older generations

  • Even in GC’d languages, programmers must be careful about unintentional reference retention, which can still cause memory leaks