A closure is a function plus an environment: a hidden record holding the variables it captured. Creating one looks like nothing — an expression, not a syscall — but the machine does three interesting things: allocate the environment, decide where it lives, and later jump through an indirection to call it.
const add = (x) => x + y; — the compiler detects y is referenced by an inner function. In JavaScript, a closure object is allocated capturing the current scope's y; in Java, a lambda capturing a local creates a hidden Object[] with one slot per captured variable; in Rust, the closure type is a struct holding y by reference or value. This is a single allocation of a few dozen bytes — the malloc-trace fast path, ~50 ns — plus, in GC languages, an object for the next nursery scavenge to consider.
The captured value must outlive the function that created it — the closure can be returned, stored, or passed elsewhere. The compiler therefore treats the environment as escaping: any reference that leaves the current frame (returned, stored in a field, passed to a call that could keep it) forces the environment onto the heap. Heap placement is the safe default: correctness before performance. The escape analysis (EA) pass — JIT-level in HotSpot/V8, compile-time in Rust/Go — is what gets to overrule it.
If EA proves the closure never escapes (it's created and invoked within the same frame — the callback you define and call inline), the environment is scalar-replaced: the captured variables live in registers or on the stack like ordinary locals, and no allocation happens at all. V8's TurboFan and the JVM's C2 do exactly this for the common array.map(x => x*2) pattern. The observable result: new-heavy hot loops that look allocation-heavy can run at zero allocation rate — this is why --trace-gc sometimes shows no GC pressure where your intuition says there should be tons.
Invocation is where the closure pays its second tax: calling add(1) is an indirect call — the address isn't known at compile time (the closure object is a runtime value), so the CPU's indirect branch predictor takes a bet. A monomorphic call site (always the same closure) predicts perfectly and costs ~nothing beyond function-call-trace's normal cost; a polymorphic site (different closures through the same call site — event handlers, comparator sort) can mispredict, costing ~15–30 cycles per miss. JITs fight this with inline caches (V8) and devirtualization (HotSpot) — turning the indirect call into a checked direct call.
The GC bill: every escaping closure is a heap object with interior pointers into the captured environment, so each is a node the collector must mark, and each capture extends the closure's lifetime graph — capturing a large object keeps that object alive as long as the closure lives. Alloc-heavy functional styles (per-item callbacks creating closures) are exactly the pattern generational collectors are built for — young-gen scavenges absorb them cheaply — but retained closures (stored in maps, attached as event listeners) are the ones that grow old-gen and lengthen full cycles.
What the machine actually does is decide in microseconds whether your closure is a local variable or a heap object, then either run it as a direct call or jump through a predicted indirection. The syntax is invisible, but every captured y is a memory placement decision the compiler makes for you — and gets right most of the time.