Sparround

Memory model: stack and heap

While a program runs, memory is split into two main regions.

Stack (call stack) — memory for function calls. Each call gets a stack frame: parameters, local variables, the return address. When the function ends, the frame is discarded immediately. The stack is fast (a pointer just moves back and forth) but limited in size (typically a few MB).

Heap — the large, free-form region where objects, arrays and collections live. Allocation is more expensive, and lifetime is not tied to a function returning — an object lives as long as something references it.

A simple rule: small fixed-size values on the stack, dynamically sized structures on the heap.

Value semantics vs reference semantics is the key to understanding data structures.

  • Value: the variable holds the value itself. Assignment makes a copy. Examples: int, double, boolean.
  • Reference: the variable holds the address of an object on the heap. Assignment copies the address — two variables look at the same object.

For example: write const a = { n: 1 } then const b = a — the address is copied, not the object. After b.n = 2, a.n is 2 as well, because a and b look at the same object on the heap.

This is why passing an object to a function lets that function mutate it: the pointer is copied, not the object. It is also the operating principle of linked lists, trees and graphs — nodes are wired together by references.

ConceptJavaScriptKotlinDart
Primitive valuesnumber, string, boolean, null, undefined, symbol, bigint — value semanticsInt, Double, Boolean etc. — on the stack when the JVM can, boxed on the heap otherwiseTechnically everything is an object, but int/double/bool/String are immutable and behave like values
ObjectsObject, Array, Map, Set, functions — heap, reference semanticsAll class instances live on the heap, reference semanticsAll objects live on the heap, reference semantics
Null safetyNone — `undefined`/`null` blow up at runtimeIn the type system: `String` vs `String?`, `?.`, `?:`, checked by the compilerSound null safety: `String` vs `String?`, `!`, `??`, checked by the compiler
Shallow copy`{...obj}`, `[...arr]`; `structuredClone()` for a deep copy`copy()` on a `data class` (shallow!), `toList()``List.of(x)`, `Map.of(m)`, `{...map}`
Garbage collectionGenerational, mark-and-sweep (the V8 engine)The JVM's GC — G1, ZGC and othersGenerational; each isolate has its own GC

Garbage collection (GC) is the automatic reclaiming of heap objects nothing references any more. All three languages (JS, Kotlin/JVM, Dart) are garbage collected; you never call free().

Modern GCs work by reachability, not reference counting: the GC walks from roots (globals, live stack frames) and removes everything it cannot reach. That's why reference cycles (a.next = b; b.prev = a) are not a problem — if both are detached from the roots, both are collected.

Memory leaks happen in GC languages too: an object is no longer needed but something still references it. Typical causes: an ever-growing global cache, event listeners that are never removed, a timer or subscription still holding a closed object.

Interview tip. The most frequent question: "Why does `arr.push(4)` work on `const arr = [1,2,3]`?" A strong answer: const freezes the binding (which address the variable points at), not the object; arr = [] would throw, arr.push(4) won't. Explaining it with the "address on the stack, object on the heap" picture lands very well.

The second classic: "When do you get a stack overflow?" — on very deep or infinite recursion, because every call takes a frame and the stack is bounded (roughly 10,000 frames in JS). Add that some languages solve this with tail-call optimisation, but most JS runtimes don't implement it in practice — so deep recursion must be rewritten iteratively or with an explicit stack.

The most common mistake: mistaking a shallow copy for a deep one. {...obj} copies only the first level; nested objects are still shared.

📚 Sources and documentation