Skip to main content

Memory Management

How a language manages memory is one of the most fundamental design decisions. Here's how different languages handle memory allocation, deallocation, and safety.

Memory Model Overview​

// GARBAGE COLLECTED (automatic)
// V8 engine uses generational GC

// Objects are heap-allocated, GC handles cleanup
let obj = { name: "John" };
obj = null; // Eligible for garbage collection

// WeakRef (allows GC to collect)
let target = { data: "important" };
let weakRef = new WeakRef(target);
weakRef.deref(); // target or undefined

// WeakMap / WeakSet (weak references)
const cache = new WeakMap();
cache.set(obj, "cached");
// When obj is GC'd, entry is removed

// FinalizationRegistry (cleanup callback)
const registry = new FinalizationRegistry((value) => {
console.log("Object collected:", value);
});
registry.register(obj, "my object");

// Memory leaks to watch for:
// - Forgotten timers/intervals
// - Closures holding references
// - Detached DOM nodes
// - Global variables

Key Takeaways​

  • GC vs ownership vs manual -- Most languages (JavaScript, Python, Go, Java, C#, Ruby, PHP) use garbage collection: you allocate, and the runtime reclaims when objects are unreachable. Rust uses compile-time ownership: each value has one owner, and the compiler enforces borrowing rules. C, C++, and Zig use manual memory: you call malloc/free, new/delete, or allocator.alloc()/allocator.free(). GC simplifies development but adds latency and non-determinism; ownership (Rust) gives safety without GC; manual management gives full control but demands discipline. Choose GC for productivity; Rust for safety and performance; C/C++/Zig for systems, embedded, or when you need predictable allocation.

  • Reference counting -- Swift uses ARC (compile-time): the compiler inserts retain/release calls, and weak/unowned break cycles. C++ has std::shared_ptr for shared ownership: auto sptr = std::make_shared<int>(42); Rust offers Rc<T> (single-threaded) and Arc<T> (thread-safe) for shared ownership. PHP and Python use refcounting as part of GC (plus cycle detection). Reference counting provides deterministic cleanup when the last reference is released but can incur overhead and cycles. Use ARC/share_ptr/Rc when you need shared ownership; prefer Rust's ownership when single ownership is sufficient.

  • Memory safety guarantees -- Rust provides memory safety without GC via ownership and borrowing: the compiler rejects use-after-free, double-free, and data races. For example, let s2 = s1; moves s1, so s1 is not usable afterward. C and C++ have no such guarantees; bugs like free(ptr); use(ptr) or buffer overflows are common. Zig's manual model is safer than C (no undefined behavior in safe code, explicit allocators) but does not prevent use-after-free. Choose Rust when memory safety is critical; use C/C++ only when you need low-level control and accept the risk; Zig sits between them.

  • Resource cleanup patterns -- RAII (C++), defer (Go, Zig), using (C#), and with (Python) tie resource lifetime to scope. C++: std::ifstream file("data.txt"); -- closed when scope exits. Zig: defer file.close();. C has no built-in pattern; you must manually pair allocate/free and handle errors. These patterns prevent leaks and simplify error paths. Prefer languages with structured cleanup when managing files, connections, or custom resources.

  • Heap vs stack allocation -- Value types (primitives, structs in C#/Swift/Rust) often live on the stack; reference types and dynamic allocations go on the heap. Go uses escape analysis to decide: return &x forces heap allocation. Zig requires an explicit allocator for heap allocation: allocator.create(i32) and allocator.destroy(ptr). C#'s stackalloc and Span<T> allow stack allocation for performance. Understanding allocation helps with performance and predictability. For hot paths, prefer stack allocation when possible; use Zig or C when you need explicit control over allocation strategy.