Memory Management - Deep Dive
Interview Tip: Memory management is critical for system performance and stability. Understanding stack vs heap, garbage collection algorithms, and how to detect memory leaks demonstrates systems-level thinking.
1. Memory Hierarchy
Memory is organized in a hierarchy based on speed and cost:
Fastest/Most Expensive
┌─────────────────┐
│ CPU Registers │ ~1 cycle, < 1 KB
├─────────────────┤
│ L1 Cache │ ~4 cycles, 32-64 KB per core
├─────────────────┤
│ L2 Cache │ ~12 cycles, 256-512 KB per core
├─────────────────┤
│ L3 Cache │ ~40 cycles, 8-32 MB shared
├─────────────────┤
│ Main Memory │ ~100-200 cycles, GB
│ (RAM) │
├─────────────────┤
│ SSD │ ~10,000 cycles, TB
├─────────────────┤
│ Hard Disk │ ~1,000,000 cycles, TB
└─────────────────┘
Slowest/Least Expensive
Cache Locality: Programs that access memory sequentially (good spatial locality) or repeatedly access the same memory (good temporal locality) run faster due to cache hits.
Virtual Memory
The OS provides an abstraction layer between physical RAM and what processes see:
- Virtual Address Space: Each process gets its own isolated address space
- Paging: Memory is divided into fixed-size pages (typically 4KB)
- Page Tables: Map virtual addresses to physical addresses
- Swap/Page File: Inactive pages moved to disk when RAM is full
- Memory Protection: Processes can't access each other's memory
2. Stack vs Heap Memory
| Aspect |
Stack |
Heap |
| Allocation |
Automatic (function calls) |
Manual (new/malloc) or GC |
| Deallocation |
Automatic (function returns) |
Manual (delete/free) or GC |
| Speed |
Very fast (pointer increment) |
Slower (search free blocks) |
| Size |
Limited (1-8 MB typical) |
Large (limited by RAM) |
| Fragmentation |
None |
Can fragment over time |
| Access Pattern |
LIFO (Last In, First Out) |
Random access |
| Thread Safety |
Each thread has its own |
Shared between threads |
| Use Case |
Local variables, function calls |
Dynamic data, large objects |
Stack Memory Example
void function_a() {
int x = 10; // Allocated on stack
char buffer[100]; // Allocated on stack
function_b(); // New stack frame pushed
} // Stack frame popped, memory freed
Stack Growth:
High Address
┌──────────────┐
│ function_a() │ x, buffer
│ frame │
├──────────────┤
│ function_b() │ New frame
│ frame │
├──────────────┤ ← Stack Pointer
│ │
│ (unused) │
│ │
└──────────────┘
Low Address
Heap Memory Example
// C++
int* ptr = new int[1000]; // Allocated on heap
delete[] ptr; // Must manually free
// Java
List<String> list = new ArrayList<>(); // Allocated on heap
// Garbage collector handles deallocation
// Python
data = [1, 2, 3, 4, 5] // Allocated on heap
# Reference counting + GC handles deallocation
Stack Overflow: Occurs when stack size limit is exceeded (deep recursion, large local arrays). Heap overflow is different - it's when you run out of heap memory.
3. Memory Allocation Strategies
Fixed-Size Allocation
Memory divided into fixed-size blocks. Fast but can waste space.
Block Size: 64 bytes
┌────┬────┬────┬────┬────┐
│ 64 │ 64 │ 64 │ 64 │ 64 │
└────┴────┴────┴────┴────┘
Allocating 100 bytes wastes 28 bytes (uses 2 blocks)
Variable-Size Allocation
Several algorithms for managing variable-sized blocks:
- First Fit: Use first free block that's large enough (fast)
- Best Fit: Use smallest free block that's large enough (reduces waste)
- Worst Fit: Use largest free block (leaves larger remaining blocks)
- Buddy System: Split/merge blocks in powers of 2 (used in Linux kernel)
Free List Example:
┌──────┬────────┬──────┬──────────┐
│ Used │ Free │ Used │ Free │
│ 100B │ 200B │ 150B │ 300B │
└──────┴────────┴──────┴──────────┘
Request 180B:
- First Fit: Uses 200B block
- Best Fit: Uses 200B block
- Worst Fit: Uses 300B block
Slab Allocation
Used for kernel objects. Pre-allocates common object sizes:
Slab for "task_struct" objects (fixed size):
┌─────┬─────┬─────┬─────┬─────┐
│ Free│ Free│ Used│ Free│ Used│
└─────┴─────┴─────┴─────┴─────┘
No fragmentation, very fast allocation
4. Garbage Collection
Automatic memory management that reclaims memory no longer in use.
Reference Counting
Track number of references to each object. When count reaches 0, free it.
// Python uses reference counting
a = [1, 2, 3] # refcount = 1
b = a # refcount = 2
c = a # refcount = 3
del a # refcount = 2
del b # refcount = 1
del c # refcount = 0 → freed
Circular Reference Problem:
a = []
b = []
a.append(b) # a references b
b.append(a) # b references a
del a, b # Both have refcount > 0, but unreachable!
Python uses cycle detection to handle this.
Mark and Sweep
Two-phase algorithm used by many GC systems:
Phase 1 - Mark:
1. Start from "roots" (global variables, stack, registers)
2. Traverse all reachable objects
3. Mark each reachable object
┌─────┐ ┌─────┐ ┌─────┐
│ A │────→│ B │────→│ C │ (All marked)
└─────┘ └─────┘ └─────┘
│
↓
┌─────┐
│ D │ (Marked)
└─────┘
┌─────┐
│ E │ (Unreachable - not marked)
└─────┘
Phase 2 - Sweep:
Free all unmarked objects (E)
Stop-the-World: Traditional mark-and-sweep pauses application threads during GC. Modern collectors minimize pause times.
Generational Garbage Collection
Based on observation: most objects die young.
┌─────────────────────────────────────────┐
│ Young Generation │
│ ┌─────────┬──────────┬──────────┐ │
│ │ Eden │ Survivor │ Survivor │ │
│ │ Space │ 0 │ 1 │ │
│ └─────────┴──────────┴──────────┘ │
│ │
│ New objects allocated here │
│ Collected frequently (minor GC) │
└─────────────────────────────────────────┘
↓ (survivors promoted)
┌─────────────────────────────────────────┐
│ Old Generation │
│ │
│ Long-lived objects │
│ Collected infrequently (major GC) │
└─────────────────────────────────────────┘
Java Generational GC Process:
- Objects allocated in Eden space
- Minor GC: Eden survivors → Survivor 0
- Next GC: Eden + Survivor 0 survivors → Survivor 1
- After N cycles, promote to Old Generation
- Major GC: Collect Old Generation (expensive)
Concurrent and Incremental GC
| Collector |
Type |
Pause Time |
Use Case |
| Serial GC |
Stop-the-world |
Long |
Single-threaded apps |
| Parallel GC |
Stop-the-world, multi-threaded |
Medium |
Throughput-focused |
| CMS (Concurrent Mark Sweep) |
Mostly concurrent |
Short |
Low-latency apps |
| G1 GC |
Generational, concurrent |
Predictable |
Large heaps, balanced |
| ZGC / Shenandoah |
Concurrent, region-based |
Sub-millisecond |
Very low latency |
Tri-Color Marking (Concurrent GC)
Allows GC to run concurrently with application threads:
White: Not yet visited
Gray: Visited, but children not scanned
Black: Visited and children scanned
Initial: All objects white, roots gray
During: Scan gray objects → mark children gray → mark self black
Final: White objects are garbage (unreachable)
┌───────┐
│ Root │ (Gray initially)
└───┬───┘
│
↓
┌───────┐ ┌───────┐
│ A │────→│ B │ (White → Gray → Black)
└───────┘ └───────┘
│
↓
┌───────┐
│ C │ (White → Gray → Black)
└───────┘
┌───────┐
│ D │ (Stays white → unreachable → free)
└───────┘
5. Memory Leaks
Memory that's allocated but never freed, eventually exhausting available memory.
Common Causes
1. Forgotten References (Java/JavaScript)
// Memory leak - static collection grows unbounded
class Cache {
private static Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
cache.put(key, value); // Never removed!
}
}
// Fix: Use WeakHashMap or implement eviction policy
private static Map<String, Object> cache =
new WeakHashMap<>();
2. Event Listeners Not Removed (JavaScript)
// Memory leak
function setupButton() {
const button = document.getElementById('myButton');
const largeData = new Array(1000000);
button.addEventListener('click', () => {
console.log(largeData.length); // Closure keeps largeData alive
});
// Listener never removed, largeData never freed
}
// Fix: Remove listener when done
function setupButton() {
const button = document.getElementById('myButton');
const handler = () => console.log('clicked');
button.addEventListener('click', handler);
// Later:
button.removeEventListener('click', handler);
}
3. Missing delete/free (C/C++)
// Memory leak
void processData() {
int* data = new int[1000];
// ... process data ...
// Missing: delete[] data;
} // Memory leaked
// Fix: Use RAII
void processData() {
std::unique_ptr<int[]> data(new int[1000]);
// ... process data ...
} // Automatically deleted
4. Circular References (without GC)
// C++: Smart pointer cycle
class Node {
std::shared_ptr<Node> next; // Strong reference
std::shared_ptr<Node> prev; // Strong reference
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // Cycle! Neither can be freed
// Fix: Use weak_ptr for back-references
class Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Weak reference breaks cycle
};
5. Unclosed Resources
// Memory leak (file descriptors, connections)
void readFile() {
FileInputStream fis = new FileInputStream("data.txt");
// ... read data ...
// Missing: fis.close();
}
// Fix: Try-with-resources (Java) or defer (Go)
void readFile() {
try (FileInputStream fis = new FileInputStream("data.txt")) {
// ... read data ...
} // Automatically closed
}
Detecting Memory Leaks
| Language |
Tools |
What to Look For |
| C/C++ |
Valgrind, AddressSanitizer, Dr. Memory |
Unfreed allocations, invalid accesses |
| Java |
VisualVM, JProfiler, Eclipse MAT |
Heap dumps, growing collections |
| JavaScript |
Chrome DevTools, heap snapshots |
Detached DOM nodes, retained objects |
| Python |
tracemalloc, objgraph, memory_profiler |
Growing object counts, ref cycles |
| Go |
pprof, runtime/trace |
Goroutine leaks, heap growth |
6. Memory Profiling
Heap Profiling (Go example)
import _ "net/http/pprof"
import "runtime/pprof"
// Enable profiling endpoint
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Take heap snapshot
f, _ := os.Create("heap.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Analyze:
// go tool pprof heap.prof
// (pprof) top10
// (pprof) list functionName
Valgrind (C/C++ example)
# Compile with debug symbols
g++ -g myprogram.cpp -o myprogram
# Run with Valgrind
valgrind --leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
./myprogram
# Output:
# ==12345== HEAP SUMMARY:
# ==12345== in use at exit: 1,000 bytes in 1 blocks
# ==12345== total heap usage: 2 allocs, 1 frees, 2,000 bytes allocated
# ==12345==
# ==12345== 1,000 bytes in 1 blocks are definitely lost
# ==12345== at malloc (vg_replace_malloc.c:299)
# ==12345== by main (myprogram.cpp:10)
Chrome DevTools (JavaScript)
1. Open DevTools → Memory tab
2. Take heap snapshot
3. Perform action that might leak
4. Take another snapshot
5. Compare snapshots
Look for:
- Objects that should have been freed but weren't
- Detached DOM nodes (nodes removed from DOM but still referenced)
- Growing arrays/objects between snapshots
- Event listeners that weren't removed
Java Heap Dump Analysis
# Generate heap dump
jmap -dump:live,format=b,file=heap.bin <pid>
# Or automatically on OutOfMemoryError
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap.hprof \
MyApplication
# Analyze with Eclipse MAT
# Look for:
# - Dominator tree (what's holding most memory)
# - Leak suspects (automated analysis)
# - Duplicate strings
# - Collection fill ratios
7. Best Practices
General Principles
1. Prefer Stack Over Heap
- Stack allocation is much faster
- Automatic cleanup
- Better cache locality
// Good: Stack allocation
void process() {
int data[100]; // Stack
// ... use data ...
} // Automatically cleaned up
// Avoid if possible: Heap allocation
void process() {
int* data = new int[100]; // Heap
// ... use data ...
delete[] data; // Manual cleanup required
}
2. Use RAII (Resource Acquisition Is Initialization)
Tie resource lifetime to object lifetime:
// C++: Use smart pointers
std::unique_ptr<Widget> widget = std::make_unique<Widget>();
// Automatically deleted when out of scope
// Java: Try-with-resources
try (Connection conn = getConnection()) {
// Use connection
} // Automatically closed
// Python: Context managers
with open('file.txt') as f:
data = f.read()
# Automatically closed
// Go: Defer
f, _ := os.Open("file.txt")
defer f.Close() // Executed when function returns
3. Minimize Object Creation
// Bad: Creates many temporary objects
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // New string each iteration!
}
// Good: Reuse buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
// JavaScript: Similar issue
let result = "";
for (let i = 0; i < 10000; i++) {
result += i; // Bad
}
// Better
const parts = [];
for (let i = 0; i < 10000; i++) {
parts.push(i);
}
const result = parts.join('');
4. Object Pooling
For frequently created/destroyed objects:
// Connection pooling
class ConnectionPool {
private Queue<Connection> available;
private int maxSize = 10;
public Connection acquire() {
if (!available.isEmpty()) {
return available.poll(); // Reuse existing
}
if (totalConnections < maxSize) {
return createNewConnection();
}
// Wait or throw exception
}
public void release(Connection conn) {
available.offer(conn); // Return to pool
}
}
// Used in:
// - Database connections
// - Thread pools
// - Buffer pools
// - Object pools for game entities
5. Clear References
// Java: Clear references in collections
class Stack {
private Object[] elements;
private int size = 0;
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Clear reference!
return result;
}
}
// JavaScript: Clear event listeners
class Component {
constructor() {
this.handler = this.onClick.bind(this);
element.addEventListener('click', this.handler);
}
destroy() {
element.removeEventListener('click', this.handler);
this.handler = null; // Clear reference
}
}
6. Use Weak References
For caches and callbacks that shouldn't prevent GC:
// Java: WeakHashMap for caches
Map<Key, Value> cache = new WeakHashMap<>();
// Entries automatically removed when key is GC'd
// JavaScript: WeakMap
const cache = new WeakMap();
const obj = {data: 'important'};
cache.set(obj, 'cached value');
// When obj is no longer referenced, entry can be GC'd
// Python: weakref
import weakref
cache = weakref.WeakValueDictionary()
obj = SomeObject()
cache['key'] = obj
# When obj is deleted, entry is automatically removed
7. Profile Before Optimizing
- Measure actual memory usage
- Identify bottlenecks with profiling tools
- Don't optimize prematurely
8. Set Memory Limits
# Java: Set heap size
java -Xms512m -Xmx2g MyApplication
# Initial Max
# Node.js: Set max old space
node --max-old-space-size=4096 app.js
# Go: Set GOGC (GC trigger percentage)
export GOGC=100 # Default: GC when heap doubles
# Docker: Limit container memory
docker run -m 512m myapp
Language-Specific Tips
C/C++
- Use
std::unique_ptr and std::shared_ptr instead of raw pointers
- Compile with AddressSanitizer during development:
-fsanitize=address
- Use
std::vector instead of manual arrays
- Avoid manual
new/delete, prefer make_unique/make_shared
Java
- Be careful with static collections (they live forever)
- Use try-with-resources for AutoCloseable resources
- Consider using
-XX:+UseG1GC for low-latency apps
- Monitor GC logs:
-Xlog:gc*
JavaScript
- Remove event listeners when components unmount
- Clear intervals and timeouts
- Avoid creating closures in loops
- Use WeakMap/WeakSet for metadata that shouldn't prevent GC
Python
- Use generators for large sequences (lazy evaluation)
- Use
__slots__ to reduce instance memory
- Be aware of reference cycles with
__del__
- Use
with statements for resource management
Go
- Be careful with goroutine leaks (blocked goroutines)
- Use
defer for cleanup
- Reuse buffers with
sync.Pool
- Profile with
pprof regularly
Common Interview Questions
Q: Explain the difference between stack and heap memory.
A: Stack is for automatic, short-lived local variables with LIFO allocation. It's fast but limited in size. Heap is for dynamic, long-lived objects with manual or GC'd allocation. It's larger but slower and can fragment. Each thread has its own stack, but the heap is shared.
Q: How does mark-and-sweep garbage collection work?
A: Two phases: (1) Mark - starting from roots (globals, stack), traverse all reachable objects and mark them. (2) Sweep - iterate through heap and free all unmarked objects. Modern variants include generational GC (young vs old), concurrent marking (tri-color), and incremental collection to reduce pause times.
Q: What causes memory leaks in garbage-collected languages?
A: Even with GC, leaks occur when objects are unintentionally retained: (1) Forgotten event listeners, (2) Static collections that grow unbounded, (3) Closures capturing large objects, (4) Unclosed resources (files, connections), (5) Caches without eviction policies. The GC can't free objects that are still reachable.
Q: How would you investigate a memory leak in production?
A: (1) Monitor metrics (heap usage, GC frequency) to confirm leak, (2) Take heap dumps/snapshots at intervals, (3) Compare snapshots to find growing objects, (4) Use profiling tools (Valgrind, Chrome DevTools, jmap), (5) Review recent code changes for common patterns (event listeners, static collections, unclosed resources), (6) Add instrumentation/logging, (7) Reproduce in staging if possible.
Q: What is memory fragmentation and how do you prevent it?
A: Fragmentation occurs when free memory is scattered in small blocks, preventing large allocations despite sufficient total free space. Internal fragmentation wastes space within allocated blocks. External fragmentation creates unusable gaps between blocks. Prevention: (1) Use memory pools for fixed-size objects, (2) Buddy system allocation, (3) Compacting GC that moves objects, (4) Generational GC that segregates by lifetime, (5) Avoid frequent allocation/deallocation of variable-sized objects.
Q: Explain virtual memory and why it's important.
A: Virtual memory provides each process with its own isolated address space mapped to physical RAM via page tables. Benefits: (1) Process isolation - processes can't access each other's memory, (2) Simplified memory management - contiguous virtual memory can map to fragmented physical memory, (3) Demand paging - only load needed pages from disk, (4) Larger address space than physical RAM via swapping, (5) Memory protection and security.
Key Takeaway: Effective memory management requires understanding the trade-offs between stack vs heap, manual vs automatic management, and throughput vs latency. Profile first, optimize based on data, and use appropriate tools and patterns for your language and use case.