Memory Models — Cross-Language Comparison
Memory Models — Cross-Language Comparison
- Type: cross-cutting comparison
- Languages covered: 51 (see _index)
- Last updated: 2026-05-07
TL;DR
- Five families dominate: manual, RAII + ownership, reference counting, tracing GC, and image/region-based. Most modern languages are hybrids.
- The biggest practical axis is who pauses your program: tracing GCs do (in microseconds-to-seconds), refcounted languages don’t (but have cycle-collection costs and atomic-decrement overhead in MT). Ownership systems push the cost to compile time.
- “GC” hides huge variation: V8’s incremental scavenge is nothing like Java ZGC’s pauseless or Erlang’s per-process heaps. Treat the family as a starting point, not a verdict.
- Several recent languages (Lean 4 FBIP, Roc uniqueness, Pony reference capabilities) sit between RC and ownership — they get RC’s predictability with ownership’s no-pause guarantee, by typing uniqueness.
- Image-based (Smalltalk/Pharo) is its own thing: the “heap” is a persistent file you snapshot and resume. There is no boundary between disk and memory.
The spectrum
manual RAII ownership refcaps RC hybrid-RC+cycles tracing-GC image-based
| | | | | | | |
C C++ Rust Pony Swift Python/Perl/PHP Java/Go/JS Pharo
Zig Crystal Idris2 ObjC ARC C#/Erlang Squeak
Odin lib:: Lean4-FBIP Ruby/Lua Common Lisp
Roc-uniq Elixir/BEAM
Categories
1. Manual / explicit
You call malloc/free (or equivalent). The compiler does not insert deallocations. Maximum control, maximum footgun surface.
- C —
malloc/free, no destructors. UB on use-after-free, double-free, leak. Sanitizers (ASan/UBSan) catch most at runtime. - C++ — manual is available (
new/delete) but RAII is the idiom; raw pointers exist for interop. - Zig — allocators are first-class parameters; you must thread an
Allocatorthrough every API. No hidden allocations. Zig’s “all allocations are explicit” is the language’s defining property. - Odin — allocator is part of an implicit
contextstruct passed everywhere; you can swap it (arena, temp, GPA) per scope. Closer to “convention-explicit” than fully manual. - Fortran —
allocate/deallocate; arrays can be auto/stack or heap-allocated.
2. RAII + smart pointers
Destructors run when an object goes out of scope. Memory cleanup is tied to control-flow, not GC. Smart pointers (unique_ptr, shared_ptr) provide higher-level disciplines on top.
- C++ — the canonical RAII language.
std::unique_ptr(move-only ownership),std::shared_ptr(refcounted),std::weak_ptr(break cycles). Custom allocators are a major axis. - Rust — RAII via
Drop, but with compiler-checked exclusive ownership (“affine types”). Values move by default; you opt into refcount withRc/Arc. See category 3. - Ada — controlled types provide RAII; Initialize/Adjust/Finalize.
- Pascal (Object Pascal/Delphi) — interfaces are refcounted (RAII-flavored); class instances are manual
Free.
3. Ownership / borrow checking
Compile-time check that exactly one owner exists at a time, with temporary borrows tracked through lifetimes. No runtime cost. Hard to learn but free at runtime.
- Rust — the canonical example. Affine types + lifetimes +
Send/Synctraits. - V — claims ownership-style autofree; in practice still WIP; falls back to GC by default as of v0.5.x.
4. Reference capabilities (typed uniqueness for actor messages)
Like ownership, but the type system tracks 6 capabilities (iso/val/ref/box/tag/trn) so the compiler can prove data-race freedom in actor message-passing. Per-actor GC underneath.
- Pony — the only mainstream language with this. ORCA distributed GC. Each actor has a private heap;
isotypes can move between actors zero-copy,valtypes share immutably.
5. Reference counting (single-threaded or atomic)
Increment on copy, decrement on drop, free at zero. No tracing pause. Cycles leak unless a cycle collector runs. Atomic increment/decrement for shared refs is non-trivial cost (~10x non-atomic).
- Swift — ARC (Automatic Reference Counting) inserted by compiler. No cycle collector — you manage cycles with
weak/unowned. - Lean — Lean 4 uses FBIP (functional but in-place): a refcount-based approach that, when the compiler proves a value has refcount 1, mutates in place. Functional code with imperative performance, no tracing GC. See Counting Immutable Beans.
- Roc — uniqueness/borrow inference informs the compiler when to mutate in place vs copy; refcounted under the hood.
- Objective-C — ARC since 2011 (mention for completeness; not in our 51).
6. Hybrid: refcount + cycle collector
Refcount handles the common case; a periodic mark-sweep collects cycles. Most “scripting” languages are this.
- Python — CPython refcounts + generational cycle collector (
gcmodule). The free-threaded build (PEP 779, 3.14) preserves this model, with atomic refcounts. - PHP — refcount + cycle GC since PHP 5.3.
- Perl — refcount; no cycle collector — circular refs leak unless you use
Scalar::Util::weaken.
7. Tracing GC — generational + young/old
Mark-and-sweep in regions, with a young generation (most allocations die young) for cheap collection. Standard for managed-runtime languages.
- Java — multiple collectors: G1 (default), Parallel, Serial; ZGC and Shenandoah are pauseless (see category 8).
- [[Languages/csharp|C#]] / .NET — server vs workstation GC; concurrent GC variants.
- Go — concurrent tri-color mark-sweep, very low pause. Not generational by design (compaction-free).
- JavaScript (V8) — Orinoco: parallel scavenge + concurrent major GC.
- TypeScript — inherits the JS runtime’s GC.
- Dart — generational scavenger; Flutter relies on its determinism.
- Kotlin — JVM GC for JVM; LLVM/native uses its own (Memory Manager, formerly ARC-based, now tracing as of 1.7.20).
- Scala — JVM GC.
- Groovy — JVM GC.
- Clojure — JVM (or BEAM, JS) GC.
- [[Languages/fsharp|F#]] — .NET GC.
- Ruby — generational + incremental + compacting (Ruby 3.x).
- Lua — incremental mark-sweep; 5.4+ adds optional generational mode.
- R — generational mark-and-sweep.
- Julia — non-moving mark-sweep, generational. Multi-threaded GC since 1.10.
- Crystal — Boehm conservative GC. Plans for precise.
- Racket — incremental, generational.
- Scheme — implementation-dependent (Chicken uses Cheney-on-the-MTA, Chez uses generational, Guile uses BDW-Boehm).
- Common Lisp — implementation-dependent (SBCL uses precise generational).
- Smalltalk (Pharo) — generational GC, plus see image-based below.
- Groovy — JVM.
8. Tracing GC — pauseless / concurrent
The collector runs concurrently with the mutator and produces pauses well under 1ms even on multi-GB heaps. Trades throughput for predictability.
- Java — ZGC (sub-ms pauses up to 16TB heaps), Shenandoah.
- Go — concurrent GC with sub-ms pauses by design (sacrifices throughput for latency).
- Erlang / Elixir — per-process heap means GC is per-process, not stop-the-world. The whole VM never pauses; one process’s GC pauses only that process.
- [[Languages/csharp|C#]] — server GC has concurrent compaction.
9. Region-based / arena
Memory is allocated in regions/arenas; a whole region is freed at once. Common in compilers, parsers, request-scoped servers.
- Nim —
arc/orcmodes provide ownership-style RC + cycle collector. Older modes (refc,markandsweep,boehm,regions,none) still selectable. - OCaml — minor heap is essentially a bump-pointer region; major heap is mark-sweep. The minor-heap design is why allocation is so cheap.
- Erlang / Elixir — each process has its own heap (a region). Process death frees the region in O(1).
- Haskell — GHC’s nursery is a bump region; promoted to older generation.
10. Image-based / persistent
The “heap” is a file. You start the system by loading the image; you save the world by writing it. There’s no source-vs-runtime distinction; the running objects are the program.
- Smalltalk (Pharo, Squeak) — the IDE, your app, and the language itself live in one image.
Smalltalk savewrites the heap to disk. - Common Lisp —
save-lisp-and-die(SBCL),dump-lisp-image(CCL) — same idea. ASDF + Quicklisp manage code; the image is the running state.
11. Linear / quantitative types — academic-leaning
Compile-time tracking of how many times a value is used (0, 1, ω). Lets the compiler reason about resource lifetime statically.
- Idris (2) — Quantitative Type Theory: every binding has a quantity (0/1/ω). Linear (1-use) values can be safely consumed without GC.
- Haskell — Linear Haskell extension (
-XLinearTypes). - Agda — quantity annotations as of 2.6+.
- Lean — does not have linear types in the Idris sense; FBIP achieves similar runtime behavior via RC analysis.
12. No real heap / stack-allocated only
- COBOL — no dynamic allocation in the classical language; modern COBOL has POINTER + ALLOCATE/FREE.
- SQL — N/A; the engine manages memory.
- Bash / PowerShell — variables are strings/objects; the shell handles memory.
- Tcl — refcounted strings/lists/dicts under the hood; you don’t see it.
Per-language quick reference
| Language | Family | Mechanism | Watch out for |
|---|---|---|---|
| Python | Hybrid RC + cycle | CPython refcount + generational cycle GC | del + cycles; PEP 703 free-threaded |
| JavaScript | Tracing | V8 Orinoco (parallel scavenge + concurrent major) | Closure retention; finalization registry |
| TypeScript | Tracing | (inherits JS runtime) | same as JS |
| Java | Tracing | G1 default; ZGC/Shenandoah pauseless | Allocation pressure; finalizers (deprecated) |
| C | Manual | malloc/free | UB galaxy; use ASan/UBSan |
| C++ | RAII + manual | unique_ptr/shared_ptr; custom allocators | Cycles in shared_ptr; iterator invalidation |
| [[Languages/csharp|C#]] | Tracing | server/workstation GC; concurrent | LOH fragmentation; GC.KeepAlive |
| Go | Tracing concurrent | concurrent tri-color mark-sweep | Goroutine leaks; finalizers are weak |
| Rust | Ownership | affine types + lifetimes; opt-in Rc/Arc | Rc<RefCell<T>> cycles still leak |
| Swift | RC | ARC; weak/unowned for cycles | Strong reference cycles |
| Kotlin | Tracing | JVM GC; native uses tracing (post-1.7.20) | Coroutine retention via captured refs |
| Ruby | Tracing | generational + incremental + compacting | ObjectSpace.each_object cost |
| PHP | Hybrid RC + cycle | refcount + cycle GC since 5.3 | gc_collect_cycles tuning |
| Scala | Tracing | JVM GC | Laziness retains classloaders |
| Dart | Tracing | generational scavenger | Flutter freeze frames |
| Elixir | Tracing per-process | per-BEAM-process heap, copy-on-send | Large message copies |
| Haskell | Tracing | GHC nursery + generational | Space leaks via thunks (! and seq) |
| Clojure | Tracing | host runtime (JVM/CLR/JS) | Holding head of lazy seq |
| [[Languages/fsharp|F#]] | Tracing | .NET GC | Same as C# |
| Lua | Tracing | incremental mark-sweep; 5.4+ generational mode | __gc finalizer ordering |
| R | Tracing | generational mark-and-sweep | Copy-on-modify can surprise |
| Julia | Tracing | non-moving mark-sweep, generational | Allocation in hot loops |
| Zig | Manual + allocators | explicit Allocator parameter | Forgotten defer |
| Nim | RC + cycle (ARC/ORC) | switchable: arc/orc/refc/boehm/markandsweep/regions/none | Use ORC for cyclic data |
| Crystal | Tracing | Boehm conservative GC | Imprecise root scanning |
| OCaml | Region + tracing | minor heap (bump) + major (mark-sweep) | Compaction triggers |
| Perl | RC | refcount; NO cycle collector | Cycles leak; use Scalar::Util::weaken |
| Erlang | Tracing per-process | per-process private heap | Atom table leaks |
| Racket | Tracing | incremental generational | Custodian-managed resources |
| Common Lisp | Tracing + image | impl-specific (SBCL: precise gen); image-saveable | Generation tuning per impl |
| Scheme | Tracing | impl-specific (Chez gen, Chicken Cheney-on-MTA, Guile BDW-Boehm) | Continuations capture stack snapshots |
| Fortran | Manual | allocate/deallocate + automatic arrays | Coarrays distributed across images |
| COBOL | Mostly static | classical = no heap; modern POINTER + ALLOCATE | Mostly N/A |
| Ada | Manual + RAII | controlled types; access types with unchecked_deallocation | Storage pools |
| Pascal | Manual + RC interfaces | manual New/Dispose; interfaces are RC | Cycles via interfaces |
| Prolog | Tracing | impl-specific (SWI uses BDW-Boehm; SICStus precise) | Term garbage in long predicates |
| Tcl | RC | internal refcounted strings/lists/dicts | Mostly invisible |
| Groovy | Tracing | JVM GC | MetaClass retention |
| Bash | N/A | shell-managed | Subshell leaks rare |
| PowerShell | Tracing | .NET GC | Pipeline-buffer growth |
| SQL | N/A | engine-managed (work_mem etc.) | Query planner memory budgets |
| V | Mixed | autofree (WIP) + opt-in GC + manual | Pre-1.0; default is GC |
| Odin | Manual + context | implicit allocator in context; arena/temp/GPA | Forgetting to swap allocator |
| Roc | RC + uniqueness | refcount; uniqueness inference for in-place mutation | Platform-controlled |
| Gleam | Tracing per-process | inherits BEAM (Erlang) | Same as Erlang |
| Pony | Refcaps + per-actor GC | 6 capabilities; ORCA distributed GC | Capability constraints |
| Idris | RC + linear | refcount; linear types for 1-use values | QTT erasure |
| Lean | RC FBIP | refcounted; in-place mutation when refcount=1 | FBIP only fires when proved unique |
| Coq (Rocq) | Tracing | OCaml runtime (mostly) | Proof terms can balloon |
| Agda | Tracing | GHC backend (mostly) | Compile-time evaluation cost |
| Smalltalk | Image + tracing | generational GC inside image; image is the persistent heap | Image bloat |
Decision rubric
Pick manual when you must control every allocation: kernels, embedded, real-time audio/games, custom databases. C, Zig, C++ raw, Odin.
Pick RAII / ownership when you want compile-time safety and zero runtime cost: high-performance services, browsers, OS components. Rust, modern C++.
Pick refcount when you want predictable destruction without compile-time complexity, AND your data is mostly tree-shaped: iOS apps (Swift), small-data scripts. Cycles are the trap.
Pick tracing GC when allocator complexity isn’t worth your engineering time: business logic, web services, data pipelines, most application code. Java, C#, Go, Python, etc.
Pick pauseless GC when you have a big heap AND tight latency requirements: HFT, real-time analytics, low-latency networking. Java ZGC, Go.
Pick per-process heap (BEAM) when you have many independent activities (millions of connections, etc.) and want fault isolation: Erlang, Elixir.
Pick image-based when you want a live system with no source-runtime split, typically for research, exploratory programming, simulation: Smalltalk, Common Lisp.
Pick refcaps / linear types when data-race freedom must be proven, not tested: Pony for actors, Idris/Linear Haskell for resource-typed APIs.
Edge cases & nuances
- A single language often spans categories. Java has G1 (gen-tracing), ZGC (pauseless), Parallel, Serial. Nim lets you pick
arc,orc,boehm, etc. at compile time. Python’s free-threaded build (3.14) makes refcounts atomic and changes the contention story. - Compaction matters. Go and ZGC don’t compact (or compact differently); Ruby 3.x does. Compaction means addresses can move — a problem for FFI.
- “Stop-the-world” is sometimes a misnomer. ZGC and Shenandoah have sub-ms STW phases but are >99% concurrent. Erlang’s whole-VM never STWs but each process pauses individually.
- Weak references / finalizers are the last resort and have surprising semantics. Java’s
finalize()is deprecated; useCleaner. Python’s__del__runs at refcount-0 (not GC), so cycles never fire it. Swift weak refs zero out automatically; RustWeakrequires upgrade. - The free-threaded Python (PEP 703) is a major shift: from refcount + GIL to atomic refcount + biased reference counting. As of 3.14 (2025-10) it’s stable; expect ecosystem fragmentation through 2027.
- Lean 4’s FBIP is the most underappreciated memory innovation in mainstream programming languages — read the Counting Immutable Beans paper.
- Pony and Roc are the two languages that found different paths to “no GC pause without ownership pain” — refcaps + per-actor GC (Pony), and uniqueness inference + RC (Roc).
Cross-references
For per-language detail, see each note’s Advanced section under “memory model & GC tuning”:
- python.md / javascript.md / typescript.md / java.md / c.md / cpp.md / csharp.md / go.md / rust.md / swift.md / kotlin.md / ruby.md / php.md / scala.md / dart.md / elixir.md / haskell.md / clojure.md / fsharp.md / lua.md / r.md / julia.md / zig.md / nim.md / crystal.md / ocaml.md / perl.md / erlang.md / racket.md / common-lisp.md / scheme.md / fortran.md / cobol.md / ada.md / pascal.md / prolog.md / tcl.md / groovy.md / bash.md / powershell.md / sql.md / v.md / odin.md / roc.md / gleam.md / pony.md / idris.md / lean.md / coq.md / agda.md / smalltalk.md
Citations
- The 51 per-language notes in _index are the underlying source of truth — facts here are derived from their Advanced sections.
- Counting Immutable Beans: Reference Counting Optimized for Purely Functional Programming — Sebastian Ullrich & Leonardo de Moura (2019). The FBIP paper underlying Lean 4’s memory model.
- The Garbage Collection Handbook — Jones, Hosking, Moss (2nd ed., 2023).
- Programming Pony — Sylvan Clebsch’s PhD thesis on reference capabilities and ORCA.
- Java HotSpot GC documentation: https://docs.oracle.com/en/java/javase/25/gctuning/
- Go runtime garbage collector: https://tip.golang.org/doc/gc-guide
- V8 Orinoco: https://v8.dev/blog/trash-talk
- PEP 703 (free-threaded Python): https://peps.python.org/pep-0703/