kae3g 9580: Memory Management - Stack, Heap, and Virtual Memory
Phase 1: Foundations & Philosophy | Week 3 | Reading Time: 17 minutes
What You'll Learn
- How processes use memory (stack vs heap)
- Virtual memory: The beautiful illusion your OS creates
- Memory allocation and deallocation
- Memory leaks and how to avoid them
- Why garbage collection matters (or doesn't)
- Memory as a finite resource (like water in a garden)
- Practical debugging with memory tools
Prerequisites
- 9500: What Is a Computer? - Memory basics
- 9570: Processes - How processes work
- 9507: Helen Atthowe - Resource management metaphor
Memory: The Finite Garden
Plant lens: Memory is like water in your garden.
- Limited resource (4GB, 16GB, 64GB RAM - but still finite!)
- Processes compete (like plants for water)
- Waste depletes it (memory leaks = water runoff)
- Must be managed carefully (drip irrigation > flood and hope)
This essay: How your OS and programs manage this precious resource.
The Memory Hierarchy
Your computer has multiple "layers" of memory (speed vs capacity trade-off):
| Type | Size | Speed | Purpose | 
|---|---|---|---|
| CPU Registers | ~100 bytes | Fastest (1 cycle) | Current operation | 
| L1 Cache | ~32 KB | Very fast (2-4 cycles) | Hot data | 
| L2 Cache | ~256 KB | Fast (10-20 cycles) | Warm data | 
| L3 Cache | ~8 MB | Moderate (40-60 cycles) | Shared cache | 
| RAM (Main Memory) | 4-64 GB | Slow (100+ cycles) | Active data | 
| SSD | 256 GB - 2 TB | Very slow (100,000+ cycles) | Persistent data | 
| HDD | 1-8 TB | Extremely slow (1,000,000+ cycles) | Cold storage | 
Key insight: CPU is starving for data (registers empty in nanoseconds). Memory hierarchy tries to keep it fed.
This essay focuses on RAM (main memory) - where your processes live.
Virtual Memory: The Beautiful Illusion
Physical memory (RAM): 16 GB (example).
Your process thinks: "I have the entire 64-bit address space!" (18 exabytes = 18 billion GB!)
How? Virtual memory - OS creates an illusion.
The Mapping
Each process has:
- Virtual address space (huge, e.g., 0x0000 to 0x7FFFFFFFFFFF)
- Page table (maps virtual addresses → physical addresses)
Example:
Virtual Address    Physical Address
0x1000_0000   →    0x0A3F_2000  (in RAM)
0x1000_1000   →    0x0B12_8000  (in RAM)
0x2000_0000   →    (not mapped - will page fault!)
0x3000_0000   →    (on disk, swap file)
Benefits:
- Isolation: Process can't access another's memory (security!)
- Simplicity: Program sees contiguous memory (even if fragmented in RAM)
- Overcommit: Total virtual memory > physical RAM (use disk as backup)
Plant lens: Like terraced gardens (each level thinks it has flat land, but it's transformed by the landscape).
Stack vs Heap
Your process memory is divided into regions:
High addresses (0x7FFF_FFFF_FFFF)
    ↑
    | Stack (grows downward)
    |   - Local variables
    |   - Function call frames
    |   - Return addresses
    |
    | (unused space)
    |
    | Heap (grows upward)
    ↓   - Dynamically allocated memory
    |   - malloc(), new, etc.
    |
    | BSS (uninitialized data)
    | Data (initialized global variables)
    | Text (code)
Low addresses (0x0000_0000)
The Stack
Automatic memory management:
void function() {
    int x = 10;        // On stack
    char buffer[100];  // On stack
    
    // When function returns, x and buffer are GONE
    // (stack frame is popped)
}
Properties:
- Fast (just adjust stack pointer)
- Limited size (typically 1-8 MB per thread)
- LIFO (Last In, First Out - like a stack of plates)
- Automatic cleanup (no memory leaks!)
Stack overflow: If you exceed stack size (e.g., infinite recursion):
void infinite() {
    int bigArray[1000000];  // 4 MB!
    infinite();  // Recurse → stack overflow
}
Plant lens: Stack is like annual plants (live one season, then gone automatically).
The Heap
Manual memory management:
void function() {
    int* x = malloc(sizeof(int));  // On heap
    *x = 10;
    
    // x still exists after function returns!
    // Must call free(x) or it leaks!
}
Properties:
- Slower (complex allocation algorithms)
- Large (gigabytes available)
- Random access (allocate/free in any order)
- Manual cleanup (or garbage collector)
Memory leak: Forgetting to free:
void leak() {
    int* x = malloc(100);
    // ... use x ...
    // Forgot to free(x)!
    // That 100 bytes is LOST until process exits
}
Plant lens: Heap is like perennial plants (live for years, must be tended or they overgrow).
Memory Allocation: Under the Hood
When you call malloc(100):
- Heap allocator searches for free block (≥100 bytes)
- Splits block if larger than needed (e.g., 256 byte block → 100 used + 156 free)
- Updates metadata (linked list of free blocks)
- Returns pointer to your memory
When you call free(ptr):
- Mark block as free
- Coalesce with adjacent free blocks (merge to reduce fragmentation)
- Update free list
Fragmentation problem:
Heap: [used 50][free 10][used 30][free 10][used 20]
You need: 20 bytes
Problem: Two 10-byte free blocks, but not contiguous!
Can't allocate 20 bytes (even though 20 bytes free total).
This is FRAGMENTATION.
Solution: Garbage collector (compacts memory) or careful allocation patterns.
Garbage Collection vs Manual Management
Manual (C, C++, Rust without GC)
You control:
int* x = malloc(100);
// ... use x ...
free(x);  // YOU must free!
Pros:
- Predictable (you know when free happens)
- Low overhead (no GC pauses)
- Fine control (can optimize for cache, etc.)
Cons:
- Error-prone (forget to free → leak, double free → crash)
- Complex (ownership rules, lifetimes)
Rust: Compile-time memory safety (borrow checker prevents leaks/dangling pointers).
Garbage Collection (Java, Python, JavaScript, Go, Clojure)
System controls:
Object x = new Object();  // Allocated
// ... use x ...
// No explicit free!
// GC reclaims it when no references remain
Pros:
- Safe (no manual free → no double-free, no leaks from forgetting)
- Simple (don't think about memory)
Cons:
- Unpredictable pauses (GC runs when it wants)
- Overhead (GC tracking uses CPU/memory)
- Less control (can't fine-tune for performance)
Trade-off: Safety vs control.
Plant lens:
- Manual = hand-watering each plant (precise, but labor-intensive)
- GC = automated irrigation (convenient, but less precise)
Memory Leaks: The Silent Killer
A memory leak: Memory allocated but never freed.
Example (JavaScript):
let cache = {};
function addToCache(key, value) {
    cache[key] = value;
    // Never remove old entries!
    // Cache grows forever → memory leak!
}
// Fix: Periodically prune cache
function pruneCache() {
    const maxSize = 1000;
    if (Object.keys(cache).length > maxSize) {
        cache = {};  // Reset
    }
}
Symptoms:
- Process memory grows over time
- Eventually: Out of memory (OOM), crash
- Slow performance (more GC pressure)
Debugging:
# Watch memory usage
top -pid <PID>
# Or:
htop  # Visual
# Heap profiler (Node.js)
node --inspect app.js
# Use Chrome DevTools → Memory tab
Plant lens: Memory leak = invasive species (grows unchecked, chokes out other plants).
Virtual Memory in Practice
Page Faults
What happens when you access unmapped memory?
int* ptr = (int*)0x10000000;  // Virtual address
*ptr = 42;  // Write to this address
Steps:
- CPU tries to access 0x10000000
- MMU (Memory Management Unit) checks page table
- Not mapped! → Page fault (trap to kernel)
- Kernel decides:- Valid but not in RAM? → Load from disk (swap in)
- Invalid address? → Segmentation fault (kill process!)
 
- Resume (if valid)
Types of page faults:
- Minor: Page in memory, just needs mapping (fast)
- Major: Page on disk, must load (slow!)
- Invalid: Illegal access (crash!)
Swapping
Physical RAM full? OS moves pages to disk (swap space).
Process A:  [Page 1] [Page 2] [Page 3] [Page 4]
           In RAM    In RAM   On disk  In RAM
# Process accesses Page 3 (on disk)
# OS swaps out Page 4 (least recently used)
# OS swaps in Page 3
Process A:  [Page 1] [Page 2] [Page 3] [Page 4]
           In RAM    In RAM   In RAM   On disk
Thrashing: Too much swapping (disk is slow!), system grinds to halt.
Plant lens: Swapping = compost bin (temporarily store what doesn't fit in active garden, retrieve when needed).
Memory Safety: Rust vs C
C (Unsafe)
Dangling pointer:
int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
*ptr = 10;  // DANGLING POINTER! (ptr points to freed memory)
            // Undefined behavior (might crash, might corrupt data)
Buffer overflow:
char buffer[10];
strcpy(buffer, "This is way too long!");  // OVERFLOW!
// Writes past buffer end, corrupts adjacent memory
// Security vulnerability!
Rust (Safe)
Ownership rules (compile-time):
fn main() {
    let x = Box::new(42);  // Heap allocation
    let y = x;  // Ownership moves to y
    
    // println!("{}", x);  // COMPILE ERROR! (x no longer owns the data)
    println!("{}", y);  // OK
}  // y dropped, memory freed automatically
Borrow checker:
fn main() {
    let mut x = vec![1, 2, 3];
    let y = &x;  // Immutable borrow
    
    // x.push(4);  // COMPILE ERROR! (can't mutate while borrowed)
    println!("{:?}", y);
}  // y's borrow ends, x can be mutated again
Result: Memory safety without garbage collection (compile-time guarantees!).
Plant lens: Rust is no-till farming (Helen Atthowe, Essay 9507) - gentle, precise intervention, no disruption.
Practical Memory Debugging
Tools
C/C++:
# Valgrind (memory leak detector)
valgrind --leak-check=full ./myprogram
# Output:
# LEAK SUMMARY:
#    definitely lost: 100 bytes in 1 blocks
#    (Shows where allocation happened)
Rust:
# Miri (interpreter that detects undefined behavior)
cargo +nightly miri run
# Or just compile (borrow checker catches most issues)
cargo build
Java:
# Heap dump
jmap -dump:live,format=b,file=heap.bin <pid>
# Analyze with VisualVM, Eclipse MAT, etc.
JavaScript (Node.js):
# Heap snapshot
node --inspect app.js
# Chrome DevTools → Memory → Take snapshot
Detecting Leaks
Pattern:
- Baseline: Measure memory at start
- Exercise: Run workload
- Idle: Let GC run, stabilize
- Compare: Memory higher than baseline? Leak!
Example (Node.js):
console.log(process.memoryUsage().heapUsed);
// 10 MB
for (let i = 0; i < 1000; i++) {
    doWork();
}
global.gc();  // Force GC (node --expose-gc)
console.log(process.memoryUsage().heapUsed);
// 50 MB (grew 40 MB - leak!)
Memory-Efficient Design
Principle 1: Reuse Allocations
Bad (allocate every time):
(defn process-items [items]
  (map (fn [item]
         (let [buffer (byte-array 1024)]  ; Allocate!
           (process item buffer)))
       items))
;; Allocates 1024 bytes × N items (wasteful!)
Good (reuse buffer):
(defn process-items [items]
  (let [buffer (byte-array 1024)]  ; Allocate once
    (map (fn [item]
           (process item buffer))  ; Reuse
         items)))
;; Allocates 1024 bytes once (efficient!)
Principle 2: Lazy Evaluation
Bad (realize entire sequence):
(def huge-data (range 1000000))  ; 1M integers in memory!
(defn process-all []
  (doall (map expensive-operation huge-data)))
;; All 1M results in memory at once!
Good (lazy, process incrementally):
(def huge-data (range 1000000))
(defn process-all []
  (doseq [item huge-data]  ; Lazy, one at a time
    (expensive-operation item)))
;; Only one item in memory at a time!
Principle 3: Avoid Defensive Copies
Bad (copy unnecessarily):
(defn get-config []
  (let [config (load-config)]
    (into {} config)))  ; COPY (defensive, but wasteful if config is immutable!)
Good (immutable, no copy needed):
(defn get-config []
  (load-config))  ; Return directly (immutable, safe to share!)
Clojure wins: Persistent data structures (structural sharing) avoid copies.
Try This
Exercise 1: Measure Your Process Memory
# Start a process
python3 -c "import time; x = [0] * 10000000; time.sleep(60)"
# (Allocates ~80 MB, sleeps 60 seconds)
# In another terminal:
ps aux | grep python3
# Look at RSS (Resident Set Size) column
# Or:
top -pid <PID>
Observe: Memory usage (RSS) shows ~80 MB.
Exercise 2: Create a Memory Leak
# leak.py
cache = []
while True:
    cache.append("x" * 1000000)  # 1 MB string
    print(f"Allocated {len(cache)} MB")
    import time
    time.sleep(1)
# Run:
python3 leak.py
# Watch memory grow (top or htop)
# Ctrl-C to stop before OOM!
Observe: Memory grows indefinitely (leak!).
Exercise 3: Profile Heap Allocation
Rust:
cargo install cargo-flamegraph
cargo flamegraph --bin myapp
# Opens flamegraph (visual heap profile)
Node.js:
node --inspect app.js
# Chrome → chrome://inspect → Open DevTools
# Memory tab → Take heap snapshot
Going Deeper
Related Essays
- 9500: What Is a Computer? - Memory hierarchy basics
- 9570: Processes - How processes use memory
- 9507: Helen Atthowe - Resource management (water = memory)
- 9955: Redox OS - Rust memory safety (narrative series)
External Resources
- "What Every Programmer Should Know About Memory" - Ulrich Drepper (deep dive)
- Valgrind documentation - Memory debugging tools
- Rust Book Chapter 4 - Ownership and borrowing
- "The Garbage Collection Handbook" - Comprehensive GC reference
Reflection Questions
- Is garbage collection "free"? (What are you trading? Convenience for...?)
- Can you have memory leaks in a GC'd language? (Yes! Holding references = memory can't be collected)
- Why does Rust's borrow checker prevent memory issues? (Compile-time tracking of ownership = no runtime errors)
- Is virtual memory always good? (Hides real limits, can lead to thrashing if overused)
- How much memory does your main project use? (Have you measured? Profiled? Optimized?)
Summary
Memory Management Concepts:
Virtual Memory:
- Each process sees huge address space (illusion!)
- Page tables map virtual → physical addresses
- Isolation (processes can't access each other's memory)
Stack vs Heap:
- Stack: Fast, automatic, limited (LIFO, function locals)
- Heap: Slower, manual/GC, large (random access, long-lived data)
Allocation Strategies:
- Manual (C/C++): Full control, error-prone
- GC (Java/Python/Clojure): Safe, overhead
- Rust: Compile-time safety, no GC!
Memory Issues:
- Leaks: Allocate but never free (grows until OOM)
- Dangling pointers: Use-after-free (undefined behavior)
- Fragmentation: Free space exists but not contiguous
Key Insights:
- Memory is finite (like water in a garden - manage carefully!)
- Virtual memory is illusion (brilliant OS abstraction)
- Stack is fast, heap is flexible (use each appropriately)
- GC trades control for safety (no one-size-fits-all)
- Rust proves GC optional (compile-time guarantees work!)
In the Valley:
- We respect memory limits (finite resource, like water)
- We profile before optimizing (observation before action - Helen's principle)
- We choose immutability (Clojure persistent data - structural sharing)
- We avoid premature allocation (lazy evaluation, reuse buffers)
Plant lens: "Memory is water—finite, must flow to all plants (processes), leaks waste it, closed-loop systems conserve it."
Next: We'll explore the filesystem—how your OS organizes persistent storage hierarchically, like a well-structured garden with paths, directories, and careful organization!
Navigation:
 ← Previous: 9570 (processes programs in motion) | Phase 1 Index | Next: 9591 (filesystem hierarchical organization)
Metadata:
- Phase: 1 (Foundations)
- Week: 3
- Prerequisites: 9500, 9570, 9507
- Concepts: Virtual memory, stack, heap, garbage collection, memory leaks, Rust ownership, page faults, swapping
- Next Concepts: Filesystem, directories, paths, inodes
- Plant Lens: Memory = water (finite resource), leaks = runoff, closed-loop = conservation, stack = annuals, heap = perennials
Copyright © 2025 kae3g | Dual-licensed under Apache-2.0 / MIT
 Competitive technology in service of clarity and beauty